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 postpatcomps_count(&self) -> rusqlite::Result<i64> {
900 self.conn
901 .query_row("SELECT COUNT(*) FROM postpatcomps", [], |row| row.get(0))
902 }
903
904 pub fn set_keycomp(&self, key: &str, function: &str) -> rusqlite::Result<()> {
910 self.conn.execute(
911 "INSERT OR REPLACE INTO keycomps (key, function) VALUES (?1, ?2)",
912 params![key, function],
913 )?;
914 Ok(())
915 }
916
917 pub fn get_keycomp(&self, key: &str) -> rusqlite::Result<Option<String>> {
919 self.conn
920 .query_row(
921 "SELECT function FROM keycomps WHERE key = ?1",
922 params![key],
923 |row| row.get(0),
924 )
925 .optional()
926 }
927
928 pub fn cache_results(&self, context: &str, data: &[u8], mtime: i64) -> rusqlite::Result<()> {
934 self.conn.execute(
935 "INSERT OR REPLACE INTO cache (context, data, mtime) VALUES (?1, ?2, ?3)",
936 params![context, data, mtime],
937 )?;
938 Ok(())
939 }
940
941 pub fn get_cached(&self, context: &str, max_age: i64) -> rusqlite::Result<Option<Vec<u8>>> {
943 let now = std::time::SystemTime::now()
944 .duration_since(std::time::UNIX_EPOCH)
945 .unwrap()
946 .as_secs() as i64;
947
948 self.conn
949 .query_row(
950 "SELECT data FROM cache WHERE context = ?1 AND mtime > ?2",
951 params![context, now - max_age],
952 |row| row.get(0),
953 )
954 .optional()
955 }
956
957 pub fn clear_stale_cache(&self, max_age: i64) -> rusqlite::Result<usize> {
959 let now = std::time::SystemTime::now()
960 .duration_since(std::time::UNIX_EPOCH)
961 .unwrap()
962 .as_secs() as i64;
963
964 self.conn
965 .execute("DELETE FROM cache WHERE mtime < ?1", params![now - max_age])
966 }
967
968 pub fn clear_cache(&self) -> rusqlite::Result<()> {
970 self.conn.execute("DELETE FROM cache", [])?;
971 Ok(())
972 }
973
974 pub fn vacuum(&self) -> rusqlite::Result<()> {
980 self.conn.execute("VACUUM", [])?;
981 Ok(())
982 }
983
984 pub fn stats(&self) -> rusqlite::Result<CacheStats> {
986 Ok(CacheStats {
987 autoloads: self.autoload_count()?,
988 zstyles: self.zstyle_count()?,
989 comps: self.comp_count()?,
990 patcomps: self
991 .conn
992 .query_row("SELECT COUNT(*) FROM patcomps", [], |r| r.get(0))?,
993 keycomps: self
994 .conn
995 .query_row("SELECT COUNT(*) FROM keycomps", [], |r| r.get(0))?,
996 services: self
997 .conn
998 .query_row("SELECT COUNT(*) FROM services", [], |r| r.get(0))?,
999 cache_entries: self
1000 .conn
1001 .query_row("SELECT COUNT(*) FROM cache", [], |r| r.get(0))?,
1002 })
1003 }
1004}
1005
1006#[derive(Debug, Clone)]
1008pub struct AutoloadStub {
1009 pub name: String,
1011 pub source: String,
1013 pub offset: i64,
1015 pub size: i64,
1017 pub body: Option<String>,
1019}
1020
1021#[derive(Debug, Clone)]
1023pub struct ZStyleEntry {
1024 pub values: Vec<String>,
1026 pub eval: bool,
1028}
1029
1030#[derive(Debug)]
1032pub struct CacheStats {
1033 pub autoloads: i64,
1035 pub zstyles: i64,
1037 pub comps: i64,
1039 pub patcomps: i64,
1041 pub keycomps: i64,
1043 pub services: i64,
1045 pub cache_entries: i64,
1047}
1048
1049fn serde_values_to_json(values: &[String]) -> String {
1051 let escaped: Vec<String> = values
1052 .iter()
1053 .map(|s| format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")))
1054 .collect();
1055 format!("[{}]", escaped.join(","))
1056}
1057
1058fn serde_json_to_values(json: &str) -> Vec<String> {
1060 let trimmed = json.trim();
1061 if !trimmed.starts_with('[') || !trimmed.ends_with(']') {
1062 return vec![json.to_string()];
1063 }
1064
1065 let inner = &trimmed[1..trimmed.len() - 1];
1066 if inner.is_empty() {
1067 return vec![];
1068 }
1069
1070 let mut values = Vec::new();
1071 let mut current = String::new();
1072 let mut in_string = false;
1073 let mut escape = false;
1074
1075 for c in inner.chars() {
1076 if escape {
1077 current.push(c);
1078 escape = false;
1079 } else if c == '\\' {
1080 escape = true;
1081 } else if c == '"' {
1082 in_string = !in_string;
1083 } else if c == ',' && !in_string {
1084 values.push(current.trim().to_string());
1085 current = String::new();
1086 } else {
1087 current.push(c);
1088 }
1089 }
1090 if !current.is_empty() {
1091 values.push(current.trim().to_string());
1092 }
1093
1094 values
1095}
1096
1097fn pattern_matches_context(pattern: &str, context: &str) -> bool {
1099 let pat_parts: Vec<&str> = pattern.split(':').collect();
1100 let ctx_parts: Vec<&str> = context.split(':').collect();
1101
1102 if pat_parts.len() > ctx_parts.len() {
1103 return false;
1104 }
1105
1106 for (p, c) in pat_parts.iter().zip(ctx_parts.iter()) {
1107 if *p != "*" && *p != *c {
1108 return false;
1109 }
1110 }
1111
1112 true
1113}
1114
1115fn calculate_pattern_weight(pattern: &str) -> i32 {
1117 let parts: Vec<&str> = pattern.split(':').filter(|s| !s.is_empty()).collect();
1118 let mut weight = parts.len() as i32 * 100;
1119
1120 for part in &parts {
1121 if *part != "*" {
1122 weight += 10;
1123 }
1124 }
1125
1126 weight
1127}
1128
1129fn glob_matches(pattern: &str, text: &str) -> bool {
1131 let mut pat_chars = pattern.chars().peekable();
1132 let mut txt_chars = text.chars().peekable();
1133
1134 while let Some(p) = pat_chars.next() {
1135 match p {
1136 '*' => {
1137 if pat_chars.peek().is_none() {
1138 return true;
1139 }
1140 while txt_chars.peek().is_some() {
1141 if glob_matches(
1142 &pat_chars.clone().collect::<String>(),
1143 &txt_chars.clone().collect::<String>(),
1144 ) {
1145 return true;
1146 }
1147 txt_chars.next();
1148 }
1149 return false;
1150 }
1151 '?' => {
1152 if txt_chars.next().is_none() {
1153 return false;
1154 }
1155 }
1156 c => {
1157 if txt_chars.next() != Some(c) {
1158 return false;
1159 }
1160 }
1161 }
1162 }
1163
1164 txt_chars.peek().is_none()
1165}
1166
1167impl CompsysCache {
1173 pub fn comps_count(&self) -> rusqlite::Result<i64> {
1175 self.comp_count()
1176 }
1177
1178 pub fn comps_keys(&self) -> rusqlite::Result<Vec<String>> {
1180 let mut stmt = self
1181 .conn
1182 .prepare("SELECT command FROM comps ORDER BY command")?;
1183 let rows = stmt.query_map([], |row| row.get(0))?;
1184 rows.collect()
1185 }
1186
1187 pub fn comps_values(&self) -> rusqlite::Result<Vec<String>> {
1189 let mut stmt = self
1190 .conn
1191 .prepare("SELECT function FROM comps ORDER BY command")?;
1192 let rows = stmt.query_map([], |row| row.get(0))?;
1193 rows.collect()
1194 }
1195
1196 pub fn comps_kv(&self) -> rusqlite::Result<Vec<(String, String)>> {
1198 let mut stmt = self
1199 .conn
1200 .prepare("SELECT command, function FROM comps ORDER BY command")?;
1201 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1202 rows.collect()
1203 }
1204
1205 pub fn patcomps_count(&self) -> rusqlite::Result<i64> {
1209 self.conn
1210 .query_row("SELECT COUNT(*) FROM patcomps", [], |row| row.get(0))
1211 }
1212
1213 pub fn patcomps_keys(&self) -> rusqlite::Result<Vec<String>> {
1215 let mut stmt = self.conn.prepare("SELECT pattern FROM patcomps")?;
1216 let rows = stmt.query_map([], |row| row.get(0))?;
1217 rows.collect()
1218 }
1219
1220 pub fn patcomps_kv(&self) -> rusqlite::Result<Vec<(String, String)>> {
1222 let mut stmt = self
1223 .conn
1224 .prepare("SELECT pattern, function FROM patcomps")?;
1225 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1226 rows.collect()
1227 }
1228
1229 pub fn set_service(&self, command: &str, service: &str) -> rusqlite::Result<()> {
1233 self.conn.execute(
1234 "INSERT OR REPLACE INTO services (command, service) VALUES (?1, ?2)",
1235 params![command, service],
1236 )?;
1237 Ok(())
1238 }
1239
1240 pub fn get_service(&self, command: &str) -> rusqlite::Result<Option<String>> {
1242 self.conn
1243 .query_row(
1244 "SELECT service FROM services WHERE command = ?1",
1245 params![command],
1246 |row| row.get(0),
1247 )
1248 .optional()
1249 }
1250
1251 pub fn services_count(&self) -> rusqlite::Result<i64> {
1253 self.conn
1254 .query_row("SELECT COUNT(*) FROM services", [], |row| row.get(0))
1255 }
1256
1257 pub fn services_keys(&self) -> rusqlite::Result<Vec<String>> {
1259 let mut stmt = self.conn.prepare("SELECT command FROM services")?;
1260 let rows = stmt.query_map([], |row| row.get(0))?;
1261 rows.collect()
1262 }
1263
1264 pub fn set_services_bulk(&mut self, services: &[(String, String)]) -> rusqlite::Result<()> {
1266 let tx = self.conn.transaction()?;
1267 {
1268 let mut stmt =
1269 tx.prepare("INSERT OR REPLACE INTO services (command, service) VALUES (?1, ?2)")?;
1270 for (command, service) in services {
1271 stmt.execute(params![command, service])?;
1272 }
1273 }
1274 tx.commit()?;
1275 Ok(())
1276 }
1277
1278 pub fn compautos_count(&self) -> rusqlite::Result<i64> {
1282 self.autoload_count()
1283 }
1284
1285 pub fn compautos_keys(&self) -> rusqlite::Result<Vec<String>> {
1287 let mut stmt = self.conn.prepare("SELECT name FROM autoloads")?;
1288 let rows = stmt.query_map([], |row| row.get(0))?;
1289 rows.collect()
1290 }
1291
1292 pub fn has_executables(&self) -> rusqlite::Result<bool> {
1298 let count: i64 = self
1299 .conn
1300 .query_row("SELECT COUNT(*) FROM executables", [], |row| row.get(0))?;
1301 Ok(count > 0)
1302 }
1303
1304 pub fn set_executables_bulk(
1306 &mut self,
1307 executables: &[(String, String)],
1308 ) -> rusqlite::Result<()> {
1309 let tx = self.conn.transaction()?;
1310 tx.execute("DELETE FROM executables", [])?;
1311 tx.execute("DELETE FROM fts_executables", [])?;
1312 {
1313 let mut stmt =
1314 tx.prepare("INSERT OR IGNORE INTO executables (name, path) VALUES (?1, ?2)")?;
1315 let mut fts_stmt =
1316 tx.prepare("INSERT OR IGNORE INTO fts_executables (name) VALUES (?1)")?;
1317 for (name, path) in executables {
1318 stmt.execute(params![name, path])?;
1319 fts_stmt.execute(params![name])?;
1320 }
1321 }
1322 tx.commit()
1323 }
1324
1325 pub fn get_executable_names(&self) -> rusqlite::Result<std::collections::HashSet<String>> {
1327 let mut stmt = self.conn.prepare("SELECT name FROM executables")?;
1328 let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
1329 rows.collect::<Result<std::collections::HashSet<_>, _>>()
1330 }
1331
1332 pub fn has_executable(&self, name: &str) -> rusqlite::Result<bool> {
1334 let exists: i64 = self.conn.query_row(
1336 "SELECT EXISTS(SELECT 1 FROM executables WHERE name = ?1)",
1337 params![name],
1338 |row| row.get(0),
1339 )?;
1340 Ok(exists == 1)
1341 }
1342
1343 pub fn get_executable_path(&self, name: &str) -> rusqlite::Result<Option<String>> {
1345 self.conn
1346 .query_row(
1347 "SELECT path FROM executables WHERE name = ?1",
1348 params![name],
1349 |row| row.get(0),
1350 )
1351 .optional()
1352 }
1353
1354 pub fn get_executables_prefix_fts(
1356 &self,
1357 prefix: &str,
1358 ) -> rusqlite::Result<Vec<(String, String)>> {
1359 if prefix.is_empty() {
1360 let mut stmt = self.conn.prepare("SELECT name, path FROM executables")?;
1361 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1362 return rows.collect();
1363 }
1364 let pattern = format!("{}*", prefix);
1365 let mut stmt = self.conn.prepare(
1366 "SELECT e.name, e.path FROM fts_executables f, executables e WHERE f.name MATCH ?1 AND e.name = f.name"
1367 )?;
1368 let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1369 rows.collect()
1370 }
1371
1372 pub fn get_executables_prefix(&self, prefix: &str) -> rusqlite::Result<Vec<(String, String)>> {
1374 if prefix.is_empty() {
1375 let mut stmt = self
1376 .conn
1377 .prepare("SELECT name, path FROM executables ORDER BY name")?;
1378 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1379 return rows.collect();
1380 }
1381 let pattern = format!("{}%", prefix);
1382 let mut stmt = self
1383 .conn
1384 .prepare("SELECT name, path FROM executables WHERE name LIKE ?1 ORDER BY name")?;
1385 let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1386 rows.collect()
1387 }
1388
1389 pub fn executables_count(&self) -> rusqlite::Result<i64> {
1391 self.conn
1392 .query_row("SELECT COUNT(*) FROM executables", [], |row| row.get(0))
1393 }
1394
1395 pub fn has_named_dirs(&self) -> rusqlite::Result<bool> {
1401 let count: i64 = self
1402 .conn
1403 .query_row("SELECT COUNT(*) FROM named_dirs", [], |row| row.get(0))?;
1404 Ok(count > 0)
1405 }
1406
1407 pub fn set_named_dirs_bulk(&mut self, dirs: &[(String, String)]) -> rusqlite::Result<()> {
1409 let tx = self.conn.transaction()?;
1410 tx.execute("DELETE FROM named_dirs", [])?;
1411 {
1412 let mut stmt = tx.prepare("INSERT INTO named_dirs (name, path) VALUES (?1, ?2)")?;
1413 for (name, path) in dirs {
1414 stmt.execute(params![name, path])?;
1415 }
1416 }
1417 tx.commit()
1418 }
1419
1420 pub fn get_named_dirs(&self) -> rusqlite::Result<Vec<(String, String)>> {
1422 let mut stmt = self
1423 .conn
1424 .prepare("SELECT name, path FROM named_dirs ORDER BY name")?;
1425 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1426 rows.collect()
1427 }
1428
1429 pub fn get_named_dirs_prefix(&self, prefix: &str) -> rusqlite::Result<Vec<(String, String)>> {
1431 if prefix.is_empty() {
1432 return self.get_named_dirs();
1433 }
1434 let pattern = format!("{}%", prefix);
1435 let mut stmt = self
1436 .conn
1437 .prepare("SELECT name, path FROM named_dirs WHERE name LIKE ?1 ORDER BY name")?;
1438 let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1439 rows.collect()
1440 }
1441
1442 pub fn named_dirs_count(&self) -> rusqlite::Result<i64> {
1444 self.conn
1445 .query_row("SELECT COUNT(*) FROM named_dirs", [], |row| row.get(0))
1446 }
1447
1448 pub fn has_shell_functions(&self) -> rusqlite::Result<bool> {
1454 let count: i64 =
1455 self.conn
1456 .query_row("SELECT COUNT(*) FROM shell_functions", [], |row| row.get(0))?;
1457 Ok(count > 0)
1458 }
1459
1460 pub fn set_shell_functions_bulk(&mut self, funcs: &[(String, String)]) -> rusqlite::Result<()> {
1462 let tx = self.conn.transaction()?;
1463 tx.execute("DELETE FROM shell_functions", [])?;
1464 tx.execute("DELETE FROM fts_shell_functions", [])?;
1465 {
1466 let mut stmt =
1467 tx.prepare("INSERT OR IGNORE INTO shell_functions (name, source) VALUES (?1, ?2)")?;
1468 let mut fts_stmt =
1469 tx.prepare("INSERT OR IGNORE INTO fts_shell_functions (name) VALUES (?1)")?;
1470 for (name, source) in funcs {
1471 stmt.execute(params![name, source])?;
1472 fts_stmt.execute(params![name])?;
1473 }
1474 }
1475 tx.commit()
1476 }
1477
1478 pub fn get_shell_function_names(&self) -> rusqlite::Result<Vec<String>> {
1480 let mut stmt = self
1481 .conn
1482 .prepare("SELECT name FROM shell_functions ORDER BY name")?;
1483 let rows = stmt.query_map([], |row| row.get(0))?;
1484 rows.collect()
1485 }
1486
1487 pub fn get_shell_functions(&self) -> rusqlite::Result<Vec<(String, String)>> {
1489 let mut stmt = self
1490 .conn
1491 .prepare("SELECT name, source FROM shell_functions ORDER BY name")?;
1492 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1493 rows.collect()
1494 }
1495
1496 pub fn get_shell_functions_prefix_fts(
1498 &self,
1499 prefix: &str,
1500 ) -> rusqlite::Result<Vec<(String, String)>> {
1501 if prefix.is_empty() {
1502 return self.get_shell_functions();
1503 }
1504 let pattern = format!("{}*", prefix);
1505 let mut stmt = self.conn.prepare(
1506 "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"
1507 )?;
1508 let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1509 rows.collect()
1510 }
1511
1512 pub fn get_shell_functions_prefix(
1514 &self,
1515 prefix: &str,
1516 ) -> rusqlite::Result<Vec<(String, String)>> {
1517 if prefix.is_empty() {
1518 return self.get_shell_functions();
1519 }
1520 let pattern = format!("{}%", prefix);
1521 let mut stmt = self
1522 .conn
1523 .prepare("SELECT name, source FROM shell_functions WHERE name LIKE ?1 ORDER BY name")?;
1524 let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1525 rows.collect()
1526 }
1527
1528 pub fn shell_functions_count(&self) -> rusqlite::Result<i64> {
1530 self.conn
1531 .query_row("SELECT COUNT(*) FROM shell_functions", [], |row| row.get(0))
1532 }
1533
1534 pub fn set_metadata(&self, key: &str, value: &str) -> rusqlite::Result<()> {
1540 self.conn.execute(
1541 "INSERT OR REPLACE INTO metadata (key, value) VALUES (?1, ?2)",
1542 params![key, value],
1543 )?;
1544 Ok(())
1545 }
1546
1547 pub fn get_metadata(&self, key: &str) -> rusqlite::Result<Option<String>> {
1549 self.conn
1550 .query_row(
1551 "SELECT value FROM metadata WHERE key = ?1",
1552 params![key],
1553 |row| row.get(0),
1554 )
1555 .optional()
1556 }
1557
1558 pub fn has_zstyles(&self) -> rusqlite::Result<bool> {
1564 let count: i64 = self
1565 .conn
1566 .query_row("SELECT COUNT(*) FROM zstyles", [], |row| row.get(0))?;
1567 Ok(count > 0)
1568 }
1569
1570 pub fn zstyles_count(&self) -> rusqlite::Result<i64> {
1572 self.conn
1573 .query_row("SELECT COUNT(*) FROM zstyles", [], |row| row.get(0))
1574 }
1575
1576 pub fn get_all_zstyles(&self) -> rusqlite::Result<Vec<(String, String, String)>> {
1578 let mut stmt = self
1579 .conn
1580 .prepare("SELECT pattern, style, value FROM zstyles ORDER BY pattern, style")?;
1581 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))?;
1582 rows.collect()
1583 }
1584}
1585
1586#[cfg(test)]
1587mod tests {
1588 use super::*;
1589
1590 #[test]
1591 fn test_cache_basic() {
1592 let cache = CompsysCache::memory().unwrap();
1593
1594 cache
1595 .add_autoload("_git", "more_src.zwc", 1024, 5000)
1596 .unwrap();
1597 cache
1598 .add_autoload("_docker", "more_src.zwc", 6024, 3000)
1599 .unwrap();
1600
1601 let stub = cache.get_autoload("_git").unwrap().unwrap();
1602 assert_eq!(stub.source, "more_src.zwc");
1603 assert_eq!(stub.offset, 1024);
1604
1605 assert!(cache.get_autoload("_nonexistent").unwrap().is_none());
1606 }
1607
1608 #[test]
1609 fn test_zstyle_cache() {
1610 let cache = CompsysCache::memory().unwrap();
1611
1612 cache
1613 .set_zstyle(":completion:*", "menu", &["select".to_string()], false)
1614 .unwrap();
1615 cache
1616 .set_zstyle(
1617 ":completion:*:descriptions",
1618 "format",
1619 &["%d".to_string()],
1620 false,
1621 )
1622 .unwrap();
1623
1624 let entry = cache
1625 .lookup_zstyle(":completion:foo", "menu")
1626 .unwrap()
1627 .unwrap();
1628 assert_eq!(entry.values, vec!["select"]);
1629
1630 let entry = cache
1631 .lookup_zstyle(":completion:foo:descriptions", "format")
1632 .unwrap()
1633 .unwrap();
1634 assert_eq!(entry.values, vec!["%d"]);
1635 }
1636
1637 #[test]
1638 fn test_zstyle_specificity() {
1639 let cache = CompsysCache::memory().unwrap();
1640
1641 cache
1642 .set_zstyle(":completion:*", "menu", &["no".to_string()], false)
1643 .unwrap();
1644 cache
1645 .set_zstyle(
1646 ":completion:*:*:*:default",
1647 "menu",
1648 &["yes".to_string()],
1649 false,
1650 )
1651 .unwrap();
1652
1653 let entry = cache
1654 .lookup_zstyle(":completion:foo:bar:baz:default", "menu")
1655 .unwrap()
1656 .unwrap();
1657 assert_eq!(entry.values, vec!["yes"]);
1658 }
1659
1660 #[test]
1661 fn test_comps_cache() {
1662 let mut cache = CompsysCache::memory().unwrap();
1663
1664 let comps = vec![
1665 ("git".to_string(), "_git".to_string()),
1666 ("docker".to_string(), "_docker".to_string()),
1667 ("cargo".to_string(), "_cargo".to_string()),
1668 ];
1669 cache.set_comps_bulk(&comps).unwrap();
1670
1671 assert_eq!(cache.get_comp("git").unwrap(), Some("_git".to_string()));
1672 assert_eq!(
1673 cache.get_comp("docker").unwrap(),
1674 Some("_docker".to_string())
1675 );
1676 assert!(cache.get_comp("nonexistent").unwrap().is_none());
1677
1678 assert_eq!(cache.comp_count().unwrap(), 3);
1679 }
1680
1681 #[test]
1682 fn test_bulk_autoloads() {
1683 let mut cache = CompsysCache::memory().unwrap();
1684
1685 let autoloads: Vec<(String, String, i64, i64)> = (0..1000)
1686 .map(|i| (format!("_func{}", i), "test.zwc".to_string(), i * 100, 100))
1687 .collect();
1688
1689 cache.add_autoloads_bulk(&autoloads).unwrap();
1690 assert_eq!(cache.autoload_count().unwrap(), 1000);
1691
1692 let stub = cache.get_autoload("_func500").unwrap().unwrap();
1693 assert_eq!(stub.offset, 50000);
1694 assert!(stub.body.is_none()); }
1696
1697 #[test]
1698 fn test_autoload_with_body() {
1699 let cache = CompsysCache::memory().unwrap();
1700
1701 let body = r#"
1702local -a opts
1703opts=(--help --version --verbose)
1704_arguments $opts
1705"#;
1706 cache
1707 .add_autoload_with_body("_mycommand", "/usr/share/zsh/functions/_mycommand", body)
1708 .unwrap();
1709
1710 let stub = cache.get_autoload("_mycommand").unwrap().unwrap();
1711 assert_eq!(stub.body.as_deref(), Some(body));
1712 assert_eq!(stub.size, body.len() as i64);
1713
1714 let direct_body = cache.get_autoload_body("_mycommand").unwrap();
1716 assert_eq!(direct_body.as_deref(), Some(body));
1717 }
1718
1719 #[test]
1720 fn test_bulk_autoloads_with_bodies() {
1721 let mut cache = CompsysCache::memory().unwrap();
1722
1723 let autoloads: Vec<(String, String, String)> = (0..100)
1724 .map(|i| {
1725 (
1726 format!("_func{}", i),
1727 format!("/path/to/_func{}", i),
1728 format!("# Function {}\necho hello", i),
1729 )
1730 })
1731 .collect();
1732
1733 cache.add_autoloads_with_bodies_bulk(&autoloads).unwrap();
1734 assert_eq!(cache.autoload_count().unwrap(), 100);
1735
1736 let stub = cache.get_autoload("_func50").unwrap().unwrap();
1737 assert!(stub.body.is_some());
1738 assert!(stub.body.unwrap().contains("Function 50"));
1739 }
1740
1741 #[test]
1742 fn test_get_autoload_body_or_zwc_with_body() {
1743 let cache = CompsysCache::memory().unwrap();
1744
1745 let body = "echo from sqlite";
1746 cache
1747 .add_autoload_with_body("_cached", "/some/path", body)
1748 .unwrap();
1749
1750 let result = cache.get_autoload_body_or_zwc("_cached");
1752 assert_eq!(result, Some(body.to_string()));
1753 }
1754
1755 #[test]
1756 fn test_get_autoload_body_or_zwc_no_body() {
1757 let cache = CompsysCache::memory().unwrap();
1758
1759 cache
1761 .add_autoload("_nocache", "nonexistent.zwc", 0, 100)
1762 .unwrap();
1763
1764 let result = cache.get_autoload_body_or_zwc("_nocache");
1766 assert!(result.is_none());
1767 }
1768
1769 #[test]
1770 fn test_get_autoload_body_or_zwc_not_found() {
1771 let cache = CompsysCache::memory().unwrap();
1772
1773 let result = cache.get_autoload_body_or_zwc("_nonexistent");
1775 assert!(result.is_none());
1776 }
1777
1778 #[test]
1779 fn test_patcomp() {
1780 let cache = CompsysCache::memory().unwrap();
1781
1782 cache.set_patcomp("git-*", "_git").unwrap();
1783 cache.set_patcomp("docker-*", "_docker").unwrap();
1784
1785 assert_eq!(
1786 cache.find_patcomp("git-commit").unwrap(),
1787 Some("_git".to_string())
1788 );
1789 assert_eq!(
1790 cache.find_patcomp("docker-compose").unwrap(),
1791 Some("_docker".to_string())
1792 );
1793 assert!(cache.find_patcomp("cargo").unwrap().is_none());
1794 }
1795
1796 #[test]
1797 fn test_glob_matches() {
1798 assert!(glob_matches("git-*", "git-commit"));
1799 assert!(glob_matches("*-compose", "docker-compose"));
1800 assert!(!glob_matches("*.rs", "zle_main"));
1803 assert!(!glob_matches("git-*", "docker-compose"));
1804 assert!(glob_matches("???", "abc"));
1805 assert!(!glob_matches("???", "abcd"));
1806 }
1807
1808 #[test]
1809 fn test_json_serde() {
1810 let values = vec!["hello".to_string(), "world".to_string()];
1811 let json = serde_values_to_json(&values);
1812 let back = serde_json_to_values(&json);
1813 assert_eq!(back, values);
1814
1815 let values = vec!["with \"quotes\"".to_string()];
1816 let json = serde_values_to_json(&values);
1817 let back = serde_json_to_values(&json);
1818 assert_eq!(back, vec!["with \"quotes\""]);
1819 }
1820
1821 #[test]
1822 fn test_stats() {
1823 let mut cache = CompsysCache::memory().unwrap();
1824
1825 cache.add_autoload("_git", "test.zwc", 0, 100).unwrap();
1826 cache
1827 .set_zstyle(":completion:*", "menu", &["select".to_string()], false)
1828 .unwrap();
1829 cache.set_comp("git", "_git").unwrap();
1830
1831 let stats = cache.stats().unwrap();
1832 assert_eq!(stats.autoloads, 1);
1833 assert_eq!(stats.zstyles, 1);
1834 assert_eq!(stats.comps, 1);
1835 }
1836
1837 #[test]
1838 fn test_large_scale() {
1839 let mut cache = CompsysCache::memory().unwrap();
1840
1841 let autoloads: Vec<(String, String, i64, i64)> = (0..10000)
1843 .map(|i| {
1844 (
1845 format!("_func{}", i),
1846 format!("src{}.zwc", i % 10),
1847 i * 50,
1848 50,
1849 )
1850 })
1851 .collect();
1852
1853 cache.add_autoloads_bulk(&autoloads).unwrap();
1854
1855 let stub = cache.get_autoload("_func9999").unwrap().unwrap();
1857 assert_eq!(stub.offset, 9999 * 50);
1858
1859 assert_eq!(cache.autoload_count().unwrap(), 10000);
1860 }
1861
1862 #[test]
1863 fn test_executables_cache() {
1864 let mut cache = CompsysCache::memory().unwrap();
1865
1866 let executables = vec![
1867 ("ls".to_string(), "/bin/ls".to_string()),
1868 ("cat".to_string(), "/bin/cat".to_string()),
1869 ("git".to_string(), "/usr/bin/git".to_string()),
1870 ];
1871 cache.set_executables_bulk(&executables).unwrap();
1872
1873 assert!(cache.has_executables().unwrap());
1874 assert!(cache.has_executable("ls").unwrap());
1875 assert!(cache.has_executable("git").unwrap());
1876 assert!(!cache.has_executable("nonexistent").unwrap());
1877
1878 assert_eq!(
1879 cache.get_executable_path("ls").unwrap(),
1880 Some("/bin/ls".to_string())
1881 );
1882 assert_eq!(cache.executables_count().unwrap(), 3);
1883 }
1884
1885 #[test]
1886 fn test_executables_prefix_search() {
1887 let mut cache = CompsysCache::memory().unwrap();
1888
1889 let executables = vec![
1890 ("git".to_string(), "/usr/bin/git".to_string()),
1891 ("gitk".to_string(), "/usr/bin/gitk".to_string()),
1892 ("grep".to_string(), "/bin/grep".to_string()),
1893 ("gzip".to_string(), "/bin/gzip".to_string()),
1894 ];
1895 cache.set_executables_bulk(&executables).unwrap();
1896
1897 let git_cmds = cache.get_executables_prefix_fts("git").unwrap();
1899 assert_eq!(git_cmds.len(), 2);
1900 assert!(git_cmds.iter().any(|(name, _)| name == "git"));
1901 assert!(git_cmds.iter().any(|(name, _)| name == "gitk"));
1902
1903 let g_cmds = cache.get_executables_prefix_fts("g").unwrap();
1904 assert_eq!(g_cmds.len(), 4);
1905 }
1906
1907 #[test]
1908 fn test_named_dirs_cache() {
1909 let mut cache = CompsysCache::memory().unwrap();
1910
1911 let dirs = vec![
1912 ("proj".to_string(), "/home/user/projects".to_string()),
1913 ("docs".to_string(), "/home/user/documents".to_string()),
1914 ];
1915 cache.set_named_dirs_bulk(&dirs).unwrap();
1916
1917 assert!(cache.has_named_dirs().unwrap());
1918
1919 let all = cache.get_named_dirs().unwrap();
1920 assert_eq!(all.len(), 2);
1921
1922 let p_dirs = cache.get_named_dirs_prefix("p").unwrap();
1923 assert_eq!(p_dirs.len(), 1);
1924 assert_eq!(p_dirs[0].0, "proj");
1925 }
1926
1927 #[test]
1928 fn test_shell_functions_cache() {
1929 let mut cache = CompsysCache::memory().unwrap();
1930
1931 let functions = vec![
1932 ("myFunc".to_string(), "/home/user/.zshrc".to_string()),
1933 (
1934 "zpwrClearList".to_string(),
1935 "/home/user/.zpwr/autoload".to_string(),
1936 ),
1937 (
1938 "zpwrTop".to_string(),
1939 "/home/user/.zpwr/autoload".to_string(),
1940 ),
1941 ];
1942 cache.set_shell_functions_bulk(&functions).unwrap();
1943
1944 assert!(cache.has_shell_functions().unwrap());
1945 assert_eq!(cache.shell_functions_count().unwrap(), 3);
1946
1947 let zpwr = cache.get_shell_functions_prefix("zpwr").unwrap();
1948 assert_eq!(zpwr.len(), 2);
1949 assert!(zpwr.iter().any(|(name, _)| name == "zpwrClearList"));
1951 assert!(zpwr.iter().any(|(name, _)| name == "zpwrTop"));
1952 }
1953
1954 #[test]
1955 fn test_metadata() {
1956 let cache = CompsysCache::memory().unwrap();
1957
1958 cache.set_metadata("version", "1.0.0").unwrap();
1959 cache.set_metadata("build_time", "2026-04-22").unwrap();
1960
1961 assert_eq!(
1962 cache.get_metadata("version").unwrap(),
1963 Some("1.0.0".to_string())
1964 );
1965 assert_eq!(
1966 cache.get_metadata("build_time").unwrap(),
1967 Some("2026-04-22".to_string())
1968 );
1969 assert_eq!(cache.get_metadata("nonexistent").unwrap(), None);
1970 }
1971
1972 #[test]
1973 fn test_comps_keys() {
1974 let mut cache = CompsysCache::memory().unwrap();
1975
1976 let comps = vec![
1977 ("git".to_string(), "_git".to_string()),
1978 ("docker".to_string(), "_docker".to_string()),
1979 ];
1980 cache.set_comps_bulk(&comps).unwrap();
1981
1982 let keys = cache.comps_keys().unwrap();
1983 assert_eq!(keys.len(), 2);
1984 assert!(keys.contains(&"docker".to_string()));
1985 assert!(keys.contains(&"git".to_string()));
1986 }
1987
1988 #[test]
1989 fn test_comps_prefix() {
1990 let mut cache = CompsysCache::memory().unwrap();
1991
1992 let comps = vec![
1993 ("git".to_string(), "_git".to_string()),
1994 ("gitk".to_string(), "_gitk".to_string()),
1995 ("docker".to_string(), "_docker".to_string()),
1996 ];
1997 cache.set_comps_bulk(&comps).unwrap();
1998
1999 let git_comps = cache.comps_prefix("git").unwrap();
2000 assert_eq!(git_comps.len(), 2);
2001 }
2002
2003 #[test]
2004 fn test_zstyles_bulk() {
2005 let mut cache = CompsysCache::memory().unwrap();
2006
2007 let styles = vec![
2008 (
2009 ":completion:*".to_string(),
2010 "menu".to_string(),
2011 vec!["select".to_string()],
2012 false,
2013 ),
2014 (
2015 ":completion:*".to_string(),
2016 "verbose".to_string(),
2017 vec!["yes".to_string()],
2018 false,
2019 ),
2020 (
2021 ":completion:*:descriptions".to_string(),
2022 "format".to_string(),
2023 vec!["%d".to_string()],
2024 false,
2025 ),
2026 ];
2027 cache.set_zstyles_bulk(&styles).unwrap();
2028
2029 assert!(cache.has_zstyles().unwrap());
2030 assert_eq!(cache.zstyles_count().unwrap(), 3);
2031 }
2032
2033 #[test]
2034 fn test_services() {
2035 let cache = CompsysCache::memory().unwrap();
2036
2037 cache.set_service("git", "scm").unwrap();
2038 cache.set_service("hg", "scm").unwrap();
2039
2040 assert_eq!(cache.get_service("git").unwrap(), Some("scm".to_string()));
2041 assert_eq!(cache.get_service("unknown").unwrap(), None);
2042 }
2043
2044 #[test]
2045 fn test_cache_overwrite() {
2046 let cache = CompsysCache::memory().unwrap();
2047
2048 cache.set_comp("git", "_git_old").unwrap();
2049 assert_eq!(cache.get_comp("git").unwrap(), Some("_git_old".to_string()));
2050
2051 cache.set_comp("git", "_git_new").unwrap();
2052 assert_eq!(cache.get_comp("git").unwrap(), Some("_git_new".to_string()));
2053 }
2054
2055 #[test]
2056 fn test_executable_names() {
2057 let mut cache = CompsysCache::memory().unwrap();
2058
2059 let executables = vec![
2060 ("alpha".to_string(), "/bin/alpha".to_string()),
2061 ("beta".to_string(), "/bin/beta".to_string()),
2062 ("gamma".to_string(), "/bin/gamma".to_string()),
2063 ];
2064 cache.set_executables_bulk(&executables).unwrap();
2065
2066 let names = cache.get_executable_names().unwrap();
2067 assert_eq!(names.len(), 3);
2068 assert!(names.contains("alpha"));
2070 assert!(names.contains("beta"));
2071 assert!(names.contains("gamma"));
2072 }
2073
2074 #[test]
2075 fn postpatcomps_do_not_land_in_patcomps() {
2076 let cache = CompsysCache::memory().unwrap();
2087 cache
2088 .set_patcomp("*/(init|rc[0-9S]#).d/*", "_init_d")
2089 .unwrap();
2090 cache
2091 .set_postpatcomp("-value-,*PATH,-default-", "_dir_list")
2092 .unwrap();
2093
2094 let pat = cache.patcomps_kv().unwrap();
2095 assert_eq!(
2096 pat,
2097 vec![("*/(init|rc[0-9S]#).d/*".to_string(), "_init_d".to_string())]
2098 );
2099
2100 let post = cache.postpatcomps_kv().unwrap();
2101 assert_eq!(
2102 post,
2103 vec![(
2104 "-value-,*PATH,-default-".to_string(),
2105 "_dir_list".to_string()
2106 )]
2107 );
2108 assert_eq!(cache.postpatcomps_count().unwrap(), 1);
2109 assert_eq!(cache.patcomps_count().unwrap(), 1);
2110 }
2111
2112 #[test]
2113 fn generation_one_completion_tables_are_rebuilt() {
2114 let mut cache = CompsysCache::memory().unwrap();
2119 cache
2120 .set_comps_bulk(&[("git".to_string(), "_git".to_string())])
2121 .unwrap();
2122 cache
2123 .set_patcomp("-value-,*PATH,-default-", "_dir_list")
2124 .unwrap();
2125 cache
2127 .conn
2128 .execute("DELETE FROM metadata WHERE key = 'completion_schema'", [])
2129 .unwrap();
2130
2131 cache.migrate_completion_tables().unwrap();
2132
2133 assert_eq!(
2134 cache.comp_count().unwrap(),
2135 0,
2136 "stale comps must be dropped"
2137 );
2138 assert_eq!(cache.patcomps_count().unwrap(), 0);
2139 assert_eq!(
2140 cache.get_metadata("completion_schema").unwrap().as_deref(),
2141 Some(CompsysCache::COMPLETION_SCHEMA_GENERATION)
2142 );
2143 }
2144}