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 Some(crate::zwc::untokenize(&buf))
615 }
616
617 pub fn autoload_count(&self) -> rusqlite::Result<i64> {
619 self.conn
620 .query_row("SELECT COUNT(*) FROM autoloads", [], |row| row.get(0))
621 }
622
623 pub fn list_autoloads(&self, limit: usize) -> rusqlite::Result<Vec<String>> {
625 let mut stmt = self.conn.prepare("SELECT name FROM autoloads LIMIT ?1")?;
626 let rows = stmt.query_map(params![limit as i64], |row| row.get(0))?;
627 rows.collect()
628 }
629
630 pub fn list_autoload_names(&self) -> rusqlite::Result<Vec<String>> {
632 let mut stmt = self.conn.prepare("SELECT name FROM autoloads")?;
633 let rows = stmt.query_map([], |row| row.get(0))?;
634 rows.collect()
635 }
636
637 pub fn set_zstyle(
643 &self,
644 pattern: &str,
645 style: &str,
646 values: &[String],
647 eval: bool,
648 ) -> rusqlite::Result<()> {
649 let value_json = serde_values_to_json(values);
650 self.conn.execute(
651 "INSERT OR REPLACE INTO zstyles (pattern, style, value, eval) VALUES (?1, ?2, ?3, ?4)",
652 params![pattern, style, value_json, eval as i32],
653 )?;
654 Ok(())
655 }
656
657 pub fn set_zstyles_bulk(
659 &mut self,
660 styles: &[(String, String, Vec<String>, bool)],
661 ) -> rusqlite::Result<()> {
662 let tx = self.conn.transaction()?;
663 {
664 let mut stmt = tx.prepare(
665 "INSERT OR REPLACE INTO zstyles (pattern, style, value, eval) VALUES (?1, ?2, ?3, ?4)"
666 )?;
667 for (pattern, style, values, eval) in styles {
668 let value_json = serde_values_to_json(values);
669 stmt.execute(params![pattern, style, value_json, *eval as i32])?;
670 }
671 }
672 tx.commit()?;
673 Ok(())
674 }
675
676 pub fn delete_zstyle(&self, pattern: &str, style: Option<&str>) -> rusqlite::Result<usize> {
678 if let Some(s) = style {
679 self.conn.execute(
680 "DELETE FROM zstyles WHERE pattern = ?1 AND style = ?2",
681 params![pattern, s],
682 )
683 } else {
684 self.conn
685 .execute("DELETE FROM zstyles WHERE pattern = ?1", params![pattern])
686 }
687 }
688
689 pub fn lookup_zstyle(
691 &self,
692 context: &str,
693 style: &str,
694 ) -> rusqlite::Result<Option<ZStyleEntry>> {
695 let mut stmt = self
696 .conn
697 .prepare("SELECT pattern, value, eval FROM zstyles WHERE style = ?1")?;
698
699 let entries: Vec<(String, String, bool)> = stmt
700 .query_map(params![style], |row| {
701 Ok((row.get(0)?, row.get(1)?, row.get::<_, i32>(2)? != 0))
702 })?
703 .filter_map(|r| r.ok())
704 .collect();
705
706 let mut best: Option<(i32, String, bool)> = None;
708 for (pattern, value, eval) in entries {
709 if pattern_matches_context(&pattern, context) {
710 let weight = calculate_pattern_weight(&pattern);
711 if best.is_none() || weight > best.as_ref().unwrap().0 {
712 best = Some((weight, value, eval));
713 }
714 }
715 }
716
717 Ok(best.map(|(_, value, eval)| ZStyleEntry {
718 values: serde_json_to_values(&value),
719 eval,
720 }))
721 }
722
723 #[allow(clippy::type_complexity)]
725 pub fn list_zstyles(&self) -> rusqlite::Result<Vec<(String, String, Vec<String>, bool)>> {
726 let mut stmt = self
727 .conn
728 .prepare("SELECT pattern, style, value, eval FROM zstyles ORDER BY pattern, style")?;
729 let rows = stmt.query_map([], |row| {
730 let pattern: String = row.get(0)?;
731 let style: String = row.get(1)?;
732 let value: String = row.get(2)?;
733 let eval: bool = row.get::<_, i32>(3)? != 0;
734 Ok((pattern, style, serde_json_to_values(&value), eval))
735 })?;
736 rows.collect()
737 }
738
739 pub fn zstyle_count(&self) -> rusqlite::Result<i64> {
741 self.conn
742 .query_row("SELECT COUNT(*) FROM zstyles", [], |row| row.get(0))
743 }
744
745 pub fn set_comp(&self, command: &str, function: &str) -> rusqlite::Result<()> {
751 self.conn.execute(
752 "INSERT OR REPLACE INTO comps (command, function) VALUES (?1, ?2)",
753 params![command, function],
754 )?;
755 Ok(())
756 }
757
758 pub fn set_comps_bulk(&mut self, comps: &[(String, String)]) -> rusqlite::Result<()> {
760 let tx = self.conn.transaction()?;
761 tx.execute("DELETE FROM comps", [])?;
763 tx.execute("DELETE FROM fts_comps", [])?;
764 {
765 let mut stmt = tx.prepare("INSERT INTO comps (command, function) VALUES (?1, ?2)")?;
766 let mut fts_stmt = tx.prepare("INSERT INTO fts_comps (command) VALUES (?1)")?;
767 for (command, function) in comps {
768 stmt.execute(params![command, function])?;
769 fts_stmt.execute(params![command])?;
770 }
771 }
772 tx.commit()
773 }
774
775 pub fn comps_prefix_fts(&self, prefix: &str) -> rusqlite::Result<Vec<(String, String)>> {
777 if prefix.is_empty() {
778 return self.comps_kv();
779 }
780 let pattern = format!("{}*", prefix);
782 let mut stmt = self.conn.prepare(
783 "SELECT c.command, c.function FROM fts_comps f, comps c WHERE f.command MATCH ?1 AND c.command = f.command"
784 )?;
785 let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
786 rows.collect()
787 }
788
789 pub fn comps_prefix(&self, prefix: &str) -> rusqlite::Result<Vec<(String, String)>> {
791 if prefix.is_empty() {
792 return self.comps_kv();
793 }
794 let pattern = format!("{}%", prefix);
795 let mut stmt = self.conn.prepare(
796 "SELECT command, function FROM comps WHERE command LIKE ?1 ORDER BY command",
797 )?;
798 let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
799 rows.collect()
800 }
801
802 pub fn get_comp(&self, command: &str) -> rusqlite::Result<Option<String>> {
804 self.conn
805 .query_row(
806 "SELECT function FROM comps WHERE command = ?1",
807 params![command],
808 |row| row.get(0),
809 )
810 .optional()
811 }
812
813 pub fn get_all_comps(&self) -> rusqlite::Result<HashMap<String, String>> {
815 let mut stmt = self.conn.prepare("SELECT command, function FROM comps")?;
816 let rows = stmt.query_map([], |row| {
817 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
818 })?;
819 let mut map = HashMap::new();
820 for row in rows {
821 let (k, v) = row?;
822 map.insert(k, v);
823 }
824 Ok(map)
825 }
826
827 pub fn comp_count(&self) -> rusqlite::Result<i64> {
829 self.conn
830 .query_row("SELECT COUNT(*) FROM comps", [], |row| row.get(0))
831 }
832
833 pub fn delete_comp(&self, command: &str) -> rusqlite::Result<usize> {
835 self.conn
836 .execute("DELETE FROM comps WHERE command = ?1", params![command])
837 }
838
839 pub fn set_patcomp(&self, pattern: &str, function: &str) -> rusqlite::Result<()> {
845 self.conn.execute(
846 "INSERT OR REPLACE INTO patcomps (pattern, function) VALUES (?1, ?2)",
847 params![pattern, function],
848 )?;
849 Ok(())
850 }
851
852 pub fn find_patcomp(&self, command: &str) -> rusqlite::Result<Option<String>> {
854 let mut stmt = self
855 .conn
856 .prepare("SELECT pattern, function FROM patcomps")?;
857 let rows = stmt.query_map([], |row| {
858 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
859 })?;
860
861 for row in rows {
862 let (pattern, function) = row?;
863 if glob_matches(&pattern, command) {
864 return Ok(Some(function));
865 }
866 }
867 Ok(None)
868 }
869
870 pub fn set_postpatcomp(&self, pattern: &str, function: &str) -> rusqlite::Result<()> {
876 self.conn.execute(
877 "INSERT OR REPLACE INTO postpatcomps (pattern, function) VALUES (?1, ?2)",
878 params![pattern, function],
879 )?;
880 Ok(())
881 }
882
883 pub fn postpatcomps_kv(&self) -> rusqlite::Result<Vec<(String, String)>> {
885 let mut stmt = self
886 .conn
887 .prepare("SELECT pattern, function FROM postpatcomps")?;
888 let rows = stmt.query_map([], |row| {
889 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
890 })?;
891 rows.collect()
892 }
893
894 pub fn services_kv(&self) -> rusqlite::Result<Vec<(String, String)>> {
896 let mut stmt = self.conn.prepare("SELECT command, service FROM services")?;
897 let rows = stmt.query_map([], |row| {
898 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
899 })?;
900 rows.collect()
901 }
902
903 pub fn postpatcomps_count(&self) -> rusqlite::Result<i64> {
905 self.conn
906 .query_row("SELECT COUNT(*) FROM postpatcomps", [], |row| row.get(0))
907 }
908
909 pub fn set_keycomp(&self, key: &str, function: &str) -> rusqlite::Result<()> {
915 self.conn.execute(
916 "INSERT OR REPLACE INTO keycomps (key, function) VALUES (?1, ?2)",
917 params![key, function],
918 )?;
919 Ok(())
920 }
921
922 pub fn get_keycomp(&self, key: &str) -> rusqlite::Result<Option<String>> {
924 self.conn
925 .query_row(
926 "SELECT function FROM keycomps WHERE key = ?1",
927 params![key],
928 |row| row.get(0),
929 )
930 .optional()
931 }
932
933 pub fn cache_results(&self, context: &str, data: &[u8], mtime: i64) -> rusqlite::Result<()> {
939 self.conn.execute(
940 "INSERT OR REPLACE INTO cache (context, data, mtime) VALUES (?1, ?2, ?3)",
941 params![context, data, mtime],
942 )?;
943 Ok(())
944 }
945
946 pub fn get_cached(&self, context: &str, max_age: i64) -> rusqlite::Result<Option<Vec<u8>>> {
948 let now = std::time::SystemTime::now()
949 .duration_since(std::time::UNIX_EPOCH)
950 .unwrap()
951 .as_secs() as i64;
952
953 self.conn
954 .query_row(
955 "SELECT data FROM cache WHERE context = ?1 AND mtime > ?2",
956 params![context, now - max_age],
957 |row| row.get(0),
958 )
959 .optional()
960 }
961
962 pub fn clear_stale_cache(&self, max_age: i64) -> rusqlite::Result<usize> {
964 let now = std::time::SystemTime::now()
965 .duration_since(std::time::UNIX_EPOCH)
966 .unwrap()
967 .as_secs() as i64;
968
969 self.conn
970 .execute("DELETE FROM cache WHERE mtime < ?1", params![now - max_age])
971 }
972
973 pub fn clear_cache(&self) -> rusqlite::Result<()> {
975 self.conn.execute("DELETE FROM cache", [])?;
976 Ok(())
977 }
978
979 pub fn vacuum(&self) -> rusqlite::Result<()> {
985 self.conn.execute("VACUUM", [])?;
986 Ok(())
987 }
988
989 pub fn stats(&self) -> rusqlite::Result<CacheStats> {
991 Ok(CacheStats {
992 autoloads: self.autoload_count()?,
993 zstyles: self.zstyle_count()?,
994 comps: self.comp_count()?,
995 patcomps: self
996 .conn
997 .query_row("SELECT COUNT(*) FROM patcomps", [], |r| r.get(0))?,
998 keycomps: self
999 .conn
1000 .query_row("SELECT COUNT(*) FROM keycomps", [], |r| r.get(0))?,
1001 services: self
1002 .conn
1003 .query_row("SELECT COUNT(*) FROM services", [], |r| r.get(0))?,
1004 cache_entries: self
1005 .conn
1006 .query_row("SELECT COUNT(*) FROM cache", [], |r| r.get(0))?,
1007 })
1008 }
1009}
1010
1011#[derive(Debug, Clone)]
1013pub struct AutoloadStub {
1014 pub name: String,
1016 pub source: String,
1018 pub offset: i64,
1020 pub size: i64,
1022 pub body: Option<String>,
1024}
1025
1026#[derive(Debug, Clone)]
1028pub struct ZStyleEntry {
1029 pub values: Vec<String>,
1031 pub eval: bool,
1033}
1034
1035#[derive(Debug)]
1037pub struct CacheStats {
1038 pub autoloads: i64,
1040 pub zstyles: i64,
1042 pub comps: i64,
1044 pub patcomps: i64,
1046 pub keycomps: i64,
1048 pub services: i64,
1050 pub cache_entries: i64,
1052}
1053
1054fn serde_values_to_json(values: &[String]) -> String {
1056 let escaped: Vec<String> = values
1057 .iter()
1058 .map(|s| format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")))
1059 .collect();
1060 format!("[{}]", escaped.join(","))
1061}
1062
1063fn serde_json_to_values(json: &str) -> Vec<String> {
1065 let trimmed = json.trim();
1066 if !trimmed.starts_with('[') || !trimmed.ends_with(']') {
1067 return vec![json.to_string()];
1068 }
1069
1070 let inner = &trimmed[1..trimmed.len() - 1];
1071 if inner.is_empty() {
1072 return vec![];
1073 }
1074
1075 let mut values = Vec::new();
1076 let mut current = String::new();
1077 let mut in_string = false;
1078 let mut escape = false;
1079
1080 for c in inner.chars() {
1081 if escape {
1082 current.push(c);
1083 escape = false;
1084 } else if c == '\\' {
1085 escape = true;
1086 } else if c == '"' {
1087 in_string = !in_string;
1088 } else if c == ',' && !in_string {
1089 values.push(current.trim().to_string());
1090 current = String::new();
1091 } else {
1092 current.push(c);
1093 }
1094 }
1095 if !current.is_empty() {
1096 values.push(current.trim().to_string());
1097 }
1098
1099 values
1100}
1101
1102fn pattern_matches_context(pattern: &str, context: &str) -> bool {
1104 let pat_parts: Vec<&str> = pattern.split(':').collect();
1105 let ctx_parts: Vec<&str> = context.split(':').collect();
1106
1107 if pat_parts.len() > ctx_parts.len() {
1108 return false;
1109 }
1110
1111 for (p, c) in pat_parts.iter().zip(ctx_parts.iter()) {
1112 if *p != "*" && *p != *c {
1113 return false;
1114 }
1115 }
1116
1117 true
1118}
1119
1120fn calculate_pattern_weight(pattern: &str) -> i32 {
1122 let parts: Vec<&str> = pattern.split(':').filter(|s| !s.is_empty()).collect();
1123 let mut weight = parts.len() as i32 * 100;
1124
1125 for part in &parts {
1126 if *part != "*" {
1127 weight += 10;
1128 }
1129 }
1130
1131 weight
1132}
1133
1134fn glob_matches(pattern: &str, text: &str) -> bool {
1136 let mut pat_chars = pattern.chars().peekable();
1137 let mut txt_chars = text.chars().peekable();
1138
1139 while let Some(p) = pat_chars.next() {
1140 match p {
1141 '*' => {
1142 if pat_chars.peek().is_none() {
1143 return true;
1144 }
1145 while txt_chars.peek().is_some() {
1146 if glob_matches(
1147 &pat_chars.clone().collect::<String>(),
1148 &txt_chars.clone().collect::<String>(),
1149 ) {
1150 return true;
1151 }
1152 txt_chars.next();
1153 }
1154 return false;
1155 }
1156 '?' => {
1157 if txt_chars.next().is_none() {
1158 return false;
1159 }
1160 }
1161 c => {
1162 if txt_chars.next() != Some(c) {
1163 return false;
1164 }
1165 }
1166 }
1167 }
1168
1169 txt_chars.peek().is_none()
1170}
1171
1172impl CompsysCache {
1178 pub fn comps_count(&self) -> rusqlite::Result<i64> {
1180 self.comp_count()
1181 }
1182
1183 pub fn comps_keys(&self) -> rusqlite::Result<Vec<String>> {
1185 let mut stmt = self
1186 .conn
1187 .prepare("SELECT command FROM comps ORDER BY command")?;
1188 let rows = stmt.query_map([], |row| row.get(0))?;
1189 rows.collect()
1190 }
1191
1192 pub fn comps_values(&self) -> rusqlite::Result<Vec<String>> {
1194 let mut stmt = self
1195 .conn
1196 .prepare("SELECT function FROM comps ORDER BY command")?;
1197 let rows = stmt.query_map([], |row| row.get(0))?;
1198 rows.collect()
1199 }
1200
1201 pub fn comps_kv(&self) -> rusqlite::Result<Vec<(String, String)>> {
1203 let mut stmt = self
1204 .conn
1205 .prepare("SELECT command, function FROM comps ORDER BY command")?;
1206 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1207 rows.collect()
1208 }
1209
1210 pub fn patcomps_count(&self) -> rusqlite::Result<i64> {
1214 self.conn
1215 .query_row("SELECT COUNT(*) FROM patcomps", [], |row| row.get(0))
1216 }
1217
1218 pub fn patcomps_keys(&self) -> rusqlite::Result<Vec<String>> {
1220 let mut stmt = self.conn.prepare("SELECT pattern FROM patcomps")?;
1221 let rows = stmt.query_map([], |row| row.get(0))?;
1222 rows.collect()
1223 }
1224
1225 pub fn patcomps_kv(&self) -> rusqlite::Result<Vec<(String, String)>> {
1227 let mut stmt = self
1228 .conn
1229 .prepare("SELECT pattern, function FROM patcomps")?;
1230 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1231 rows.collect()
1232 }
1233
1234 pub fn set_service(&self, command: &str, service: &str) -> rusqlite::Result<()> {
1238 self.conn.execute(
1239 "INSERT OR REPLACE INTO services (command, service) VALUES (?1, ?2)",
1240 params![command, service],
1241 )?;
1242 Ok(())
1243 }
1244
1245 pub fn get_service(&self, command: &str) -> rusqlite::Result<Option<String>> {
1247 self.conn
1248 .query_row(
1249 "SELECT service FROM services WHERE command = ?1",
1250 params![command],
1251 |row| row.get(0),
1252 )
1253 .optional()
1254 }
1255
1256 pub fn services_count(&self) -> rusqlite::Result<i64> {
1258 self.conn
1259 .query_row("SELECT COUNT(*) FROM services", [], |row| row.get(0))
1260 }
1261
1262 pub fn services_keys(&self) -> rusqlite::Result<Vec<String>> {
1264 let mut stmt = self.conn.prepare("SELECT command FROM services")?;
1265 let rows = stmt.query_map([], |row| row.get(0))?;
1266 rows.collect()
1267 }
1268
1269 pub fn set_services_bulk(&mut self, services: &[(String, String)]) -> rusqlite::Result<()> {
1271 let tx = self.conn.transaction()?;
1272 {
1273 let mut stmt =
1274 tx.prepare("INSERT OR REPLACE INTO services (command, service) VALUES (?1, ?2)")?;
1275 for (command, service) in services {
1276 stmt.execute(params![command, service])?;
1277 }
1278 }
1279 tx.commit()?;
1280 Ok(())
1281 }
1282
1283 pub fn compautos_count(&self) -> rusqlite::Result<i64> {
1287 self.autoload_count()
1288 }
1289
1290 pub fn compautos_keys(&self) -> rusqlite::Result<Vec<String>> {
1292 let mut stmt = self.conn.prepare("SELECT name FROM autoloads")?;
1293 let rows = stmt.query_map([], |row| row.get(0))?;
1294 rows.collect()
1295 }
1296
1297 pub fn has_executables(&self) -> rusqlite::Result<bool> {
1303 let count: i64 = self
1304 .conn
1305 .query_row("SELECT COUNT(*) FROM executables", [], |row| row.get(0))?;
1306 Ok(count > 0)
1307 }
1308
1309 pub fn set_executables_bulk(
1311 &mut self,
1312 executables: &[(String, String)],
1313 ) -> rusqlite::Result<()> {
1314 let tx = self.conn.transaction()?;
1315 tx.execute("DELETE FROM executables", [])?;
1316 tx.execute("DELETE FROM fts_executables", [])?;
1317 {
1318 let mut stmt =
1319 tx.prepare("INSERT OR IGNORE INTO executables (name, path) VALUES (?1, ?2)")?;
1320 let mut fts_stmt =
1321 tx.prepare("INSERT OR IGNORE INTO fts_executables (name) VALUES (?1)")?;
1322 for (name, path) in executables {
1323 stmt.execute(params![name, path])?;
1324 fts_stmt.execute(params![name])?;
1325 }
1326 }
1327 tx.commit()
1328 }
1329
1330 pub fn get_executable_names(&self) -> rusqlite::Result<std::collections::HashSet<String>> {
1332 let mut stmt = self.conn.prepare("SELECT name FROM executables")?;
1333 let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
1334 rows.collect::<Result<std::collections::HashSet<_>, _>>()
1335 }
1336
1337 pub fn has_executable(&self, name: &str) -> rusqlite::Result<bool> {
1339 let exists: i64 = self.conn.query_row(
1341 "SELECT EXISTS(SELECT 1 FROM executables WHERE name = ?1)",
1342 params![name],
1343 |row| row.get(0),
1344 )?;
1345 Ok(exists == 1)
1346 }
1347
1348 pub fn get_executable_path(&self, name: &str) -> rusqlite::Result<Option<String>> {
1350 self.conn
1351 .query_row(
1352 "SELECT path FROM executables WHERE name = ?1",
1353 params![name],
1354 |row| row.get(0),
1355 )
1356 .optional()
1357 }
1358
1359 pub fn get_executables_prefix_fts(
1361 &self,
1362 prefix: &str,
1363 ) -> rusqlite::Result<Vec<(String, String)>> {
1364 if prefix.is_empty() {
1365 let mut stmt = self.conn.prepare("SELECT name, path FROM executables")?;
1366 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1367 return rows.collect();
1368 }
1369 let pattern = format!("{}*", prefix);
1370 let mut stmt = self.conn.prepare(
1371 "SELECT e.name, e.path FROM fts_executables f, executables e WHERE f.name MATCH ?1 AND e.name = f.name"
1372 )?;
1373 let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1374 rows.collect()
1375 }
1376
1377 pub fn get_executables_prefix(&self, prefix: &str) -> rusqlite::Result<Vec<(String, String)>> {
1379 if prefix.is_empty() {
1380 let mut stmt = self
1381 .conn
1382 .prepare("SELECT name, path FROM executables ORDER BY name")?;
1383 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1384 return rows.collect();
1385 }
1386 let pattern = format!("{}%", prefix);
1387 let mut stmt = self
1388 .conn
1389 .prepare("SELECT name, path FROM executables WHERE name LIKE ?1 ORDER BY name")?;
1390 let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1391 rows.collect()
1392 }
1393
1394 pub fn executables_count(&self) -> rusqlite::Result<i64> {
1396 self.conn
1397 .query_row("SELECT COUNT(*) FROM executables", [], |row| row.get(0))
1398 }
1399
1400 pub fn has_named_dirs(&self) -> rusqlite::Result<bool> {
1406 let count: i64 = self
1407 .conn
1408 .query_row("SELECT COUNT(*) FROM named_dirs", [], |row| row.get(0))?;
1409 Ok(count > 0)
1410 }
1411
1412 pub fn set_named_dirs_bulk(&mut self, dirs: &[(String, String)]) -> rusqlite::Result<()> {
1414 let tx = self.conn.transaction()?;
1415 tx.execute("DELETE FROM named_dirs", [])?;
1416 {
1417 let mut stmt = tx.prepare("INSERT INTO named_dirs (name, path) VALUES (?1, ?2)")?;
1418 for (name, path) in dirs {
1419 stmt.execute(params![name, path])?;
1420 }
1421 }
1422 tx.commit()
1423 }
1424
1425 pub fn get_named_dirs(&self) -> rusqlite::Result<Vec<(String, String)>> {
1427 let mut stmt = self
1428 .conn
1429 .prepare("SELECT name, path FROM named_dirs ORDER BY name")?;
1430 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1431 rows.collect()
1432 }
1433
1434 pub fn get_named_dirs_prefix(&self, prefix: &str) -> rusqlite::Result<Vec<(String, String)>> {
1436 if prefix.is_empty() {
1437 return self.get_named_dirs();
1438 }
1439 let pattern = format!("{}%", prefix);
1440 let mut stmt = self
1441 .conn
1442 .prepare("SELECT name, path FROM named_dirs WHERE name LIKE ?1 ORDER BY name")?;
1443 let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1444 rows.collect()
1445 }
1446
1447 pub fn named_dirs_count(&self) -> rusqlite::Result<i64> {
1449 self.conn
1450 .query_row("SELECT COUNT(*) FROM named_dirs", [], |row| row.get(0))
1451 }
1452
1453 pub fn has_shell_functions(&self) -> rusqlite::Result<bool> {
1459 let count: i64 =
1460 self.conn
1461 .query_row("SELECT COUNT(*) FROM shell_functions", [], |row| row.get(0))?;
1462 Ok(count > 0)
1463 }
1464
1465 pub fn set_shell_functions_bulk(&mut self, funcs: &[(String, String)]) -> rusqlite::Result<()> {
1467 let tx = self.conn.transaction()?;
1468 tx.execute("DELETE FROM shell_functions", [])?;
1469 tx.execute("DELETE FROM fts_shell_functions", [])?;
1470 {
1471 let mut stmt =
1472 tx.prepare("INSERT OR IGNORE INTO shell_functions (name, source) VALUES (?1, ?2)")?;
1473 let mut fts_stmt =
1474 tx.prepare("INSERT OR IGNORE INTO fts_shell_functions (name) VALUES (?1)")?;
1475 for (name, source) in funcs {
1476 stmt.execute(params![name, source])?;
1477 fts_stmt.execute(params![name])?;
1478 }
1479 }
1480 tx.commit()
1481 }
1482
1483 pub fn get_shell_function_names(&self) -> rusqlite::Result<Vec<String>> {
1485 let mut stmt = self
1486 .conn
1487 .prepare("SELECT name FROM shell_functions ORDER BY name")?;
1488 let rows = stmt.query_map([], |row| row.get(0))?;
1489 rows.collect()
1490 }
1491
1492 pub fn get_shell_functions(&self) -> rusqlite::Result<Vec<(String, String)>> {
1494 let mut stmt = self
1495 .conn
1496 .prepare("SELECT name, source FROM shell_functions ORDER BY name")?;
1497 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1498 rows.collect()
1499 }
1500
1501 pub fn get_shell_functions_prefix_fts(
1503 &self,
1504 prefix: &str,
1505 ) -> rusqlite::Result<Vec<(String, String)>> {
1506 if prefix.is_empty() {
1507 return self.get_shell_functions();
1508 }
1509 let pattern = format!("{}*", prefix);
1510 let mut stmt = self.conn.prepare(
1511 "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"
1512 )?;
1513 let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1514 rows.collect()
1515 }
1516
1517 pub fn get_shell_functions_prefix(
1519 &self,
1520 prefix: &str,
1521 ) -> rusqlite::Result<Vec<(String, String)>> {
1522 if prefix.is_empty() {
1523 return self.get_shell_functions();
1524 }
1525 let pattern = format!("{}%", prefix);
1526 let mut stmt = self
1527 .conn
1528 .prepare("SELECT name, source FROM shell_functions WHERE name LIKE ?1 ORDER BY name")?;
1529 let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1530 rows.collect()
1531 }
1532
1533 pub fn shell_functions_count(&self) -> rusqlite::Result<i64> {
1535 self.conn
1536 .query_row("SELECT COUNT(*) FROM shell_functions", [], |row| row.get(0))
1537 }
1538
1539 pub fn set_metadata(&self, key: &str, value: &str) -> rusqlite::Result<()> {
1545 self.conn.execute(
1546 "INSERT OR REPLACE INTO metadata (key, value) VALUES (?1, ?2)",
1547 params![key, value],
1548 )?;
1549 Ok(())
1550 }
1551
1552 pub fn get_metadata(&self, key: &str) -> rusqlite::Result<Option<String>> {
1554 self.conn
1555 .query_row(
1556 "SELECT value FROM metadata WHERE key = ?1",
1557 params![key],
1558 |row| row.get(0),
1559 )
1560 .optional()
1561 }
1562
1563 pub fn has_zstyles(&self) -> rusqlite::Result<bool> {
1569 let count: i64 = self
1570 .conn
1571 .query_row("SELECT COUNT(*) FROM zstyles", [], |row| row.get(0))?;
1572 Ok(count > 0)
1573 }
1574
1575 pub fn zstyles_count(&self) -> rusqlite::Result<i64> {
1577 self.conn
1578 .query_row("SELECT COUNT(*) FROM zstyles", [], |row| row.get(0))
1579 }
1580
1581 pub fn get_all_zstyles(&self) -> rusqlite::Result<Vec<(String, String, String)>> {
1583 let mut stmt = self
1584 .conn
1585 .prepare("SELECT pattern, style, value FROM zstyles ORDER BY pattern, style")?;
1586 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))?;
1587 rows.collect()
1588 }
1589}
1590
1591#[cfg(test)]
1592mod tests {
1593 use super::*;
1594
1595 #[test]
1596 fn test_cache_basic() {
1597 let cache = CompsysCache::memory().unwrap();
1598
1599 cache
1600 .add_autoload("_git", "more_src.zwc", 1024, 5000)
1601 .unwrap();
1602 cache
1603 .add_autoload("_docker", "more_src.zwc", 6024, 3000)
1604 .unwrap();
1605
1606 let stub = cache.get_autoload("_git").unwrap().unwrap();
1607 assert_eq!(stub.source, "more_src.zwc");
1608 assert_eq!(stub.offset, 1024);
1609
1610 assert!(cache.get_autoload("_nonexistent").unwrap().is_none());
1611 }
1612
1613 #[test]
1614 fn test_zstyle_cache() {
1615 let cache = CompsysCache::memory().unwrap();
1616
1617 cache
1618 .set_zstyle(":completion:*", "menu", &["select".to_string()], false)
1619 .unwrap();
1620 cache
1621 .set_zstyle(
1622 ":completion:*:descriptions",
1623 "format",
1624 &["%d".to_string()],
1625 false,
1626 )
1627 .unwrap();
1628
1629 let entry = cache
1630 .lookup_zstyle(":completion:foo", "menu")
1631 .unwrap()
1632 .unwrap();
1633 assert_eq!(entry.values, vec!["select"]);
1634
1635 let entry = cache
1636 .lookup_zstyle(":completion:foo:descriptions", "format")
1637 .unwrap()
1638 .unwrap();
1639 assert_eq!(entry.values, vec!["%d"]);
1640 }
1641
1642 #[test]
1643 fn test_zstyle_specificity() {
1644 let cache = CompsysCache::memory().unwrap();
1645
1646 cache
1647 .set_zstyle(":completion:*", "menu", &["no".to_string()], false)
1648 .unwrap();
1649 cache
1650 .set_zstyle(
1651 ":completion:*:*:*:default",
1652 "menu",
1653 &["yes".to_string()],
1654 false,
1655 )
1656 .unwrap();
1657
1658 let entry = cache
1659 .lookup_zstyle(":completion:foo:bar:baz:default", "menu")
1660 .unwrap()
1661 .unwrap();
1662 assert_eq!(entry.values, vec!["yes"]);
1663 }
1664
1665 #[test]
1666 fn test_comps_cache() {
1667 let mut cache = CompsysCache::memory().unwrap();
1668
1669 let comps = vec![
1670 ("git".to_string(), "_git".to_string()),
1671 ("docker".to_string(), "_docker".to_string()),
1672 ("cargo".to_string(), "_cargo".to_string()),
1673 ];
1674 cache.set_comps_bulk(&comps).unwrap();
1675
1676 assert_eq!(cache.get_comp("git").unwrap(), Some("_git".to_string()));
1677 assert_eq!(
1678 cache.get_comp("docker").unwrap(),
1679 Some("_docker".to_string())
1680 );
1681 assert!(cache.get_comp("nonexistent").unwrap().is_none());
1682
1683 assert_eq!(cache.comp_count().unwrap(), 3);
1684 }
1685
1686 #[test]
1687 fn test_bulk_autoloads() {
1688 let mut cache = CompsysCache::memory().unwrap();
1689
1690 let autoloads: Vec<(String, String, i64, i64)> = (0..1000)
1691 .map(|i| (format!("_func{}", i), "test.zwc".to_string(), i * 100, 100))
1692 .collect();
1693
1694 cache.add_autoloads_bulk(&autoloads).unwrap();
1695 assert_eq!(cache.autoload_count().unwrap(), 1000);
1696
1697 let stub = cache.get_autoload("_func500").unwrap().unwrap();
1698 assert_eq!(stub.offset, 50000);
1699 assert!(stub.body.is_none()); }
1701
1702 #[test]
1703 fn test_autoload_with_body() {
1704 let cache = CompsysCache::memory().unwrap();
1705
1706 let body = r#"
1707local -a opts
1708opts=(--help --version --verbose)
1709_arguments $opts
1710"#;
1711 cache
1712 .add_autoload_with_body("_mycommand", "/usr/share/zsh/functions/_mycommand", body)
1713 .unwrap();
1714
1715 let stub = cache.get_autoload("_mycommand").unwrap().unwrap();
1716 assert_eq!(stub.body.as_deref(), Some(body));
1717 assert_eq!(stub.size, body.len() as i64);
1718
1719 let direct_body = cache.get_autoload_body("_mycommand").unwrap();
1721 assert_eq!(direct_body.as_deref(), Some(body));
1722 }
1723
1724 #[test]
1725 fn test_bulk_autoloads_with_bodies() {
1726 let mut cache = CompsysCache::memory().unwrap();
1727
1728 let autoloads: Vec<(String, String, String)> = (0..100)
1729 .map(|i| {
1730 (
1731 format!("_func{}", i),
1732 format!("/path/to/_func{}", i),
1733 format!("# Function {}\necho hello", i),
1734 )
1735 })
1736 .collect();
1737
1738 cache.add_autoloads_with_bodies_bulk(&autoloads).unwrap();
1739 assert_eq!(cache.autoload_count().unwrap(), 100);
1740
1741 let stub = cache.get_autoload("_func50").unwrap().unwrap();
1742 assert!(stub.body.is_some());
1743 assert!(stub.body.unwrap().contains("Function 50"));
1744 }
1745
1746 #[test]
1747 fn test_get_autoload_body_or_zwc_with_body() {
1748 let cache = CompsysCache::memory().unwrap();
1749
1750 let body = "echo from sqlite";
1751 cache
1752 .add_autoload_with_body("_cached", "/some/path", body)
1753 .unwrap();
1754
1755 let result = cache.get_autoload_body_or_zwc("_cached");
1757 assert_eq!(result, Some(body.to_string()));
1758 }
1759
1760 #[test]
1761 fn test_get_autoload_body_or_zwc_no_body() {
1762 let cache = CompsysCache::memory().unwrap();
1763
1764 cache
1766 .add_autoload("_nocache", "nonexistent.zwc", 0, 100)
1767 .unwrap();
1768
1769 let result = cache.get_autoload_body_or_zwc("_nocache");
1771 assert!(result.is_none());
1772 }
1773
1774 #[test]
1775 fn test_get_autoload_body_or_zwc_not_found() {
1776 let cache = CompsysCache::memory().unwrap();
1777
1778 let result = cache.get_autoload_body_or_zwc("_nonexistent");
1780 assert!(result.is_none());
1781 }
1782
1783 #[test]
1784 fn test_patcomp() {
1785 let cache = CompsysCache::memory().unwrap();
1786
1787 cache.set_patcomp("git-*", "_git").unwrap();
1788 cache.set_patcomp("docker-*", "_docker").unwrap();
1789
1790 assert_eq!(
1791 cache.find_patcomp("git-commit").unwrap(),
1792 Some("_git".to_string())
1793 );
1794 assert_eq!(
1795 cache.find_patcomp("docker-compose").unwrap(),
1796 Some("_docker".to_string())
1797 );
1798 assert!(cache.find_patcomp("cargo").unwrap().is_none());
1799 }
1800
1801 #[test]
1802 fn test_glob_matches() {
1803 assert!(glob_matches("git-*", "git-commit"));
1804 assert!(glob_matches("*-compose", "docker-compose"));
1805 assert!(!glob_matches("*.rs", "zle_main"));
1808 assert!(!glob_matches("git-*", "docker-compose"));
1809 assert!(glob_matches("???", "abc"));
1810 assert!(!glob_matches("???", "abcd"));
1811 }
1812
1813 #[test]
1814 fn test_json_serde() {
1815 let values = vec!["hello".to_string(), "world".to_string()];
1816 let json = serde_values_to_json(&values);
1817 let back = serde_json_to_values(&json);
1818 assert_eq!(back, values);
1819
1820 let values = vec!["with \"quotes\"".to_string()];
1821 let json = serde_values_to_json(&values);
1822 let back = serde_json_to_values(&json);
1823 assert_eq!(back, vec!["with \"quotes\""]);
1824 }
1825
1826 #[test]
1827 fn test_stats() {
1828 let mut cache = CompsysCache::memory().unwrap();
1829
1830 cache.add_autoload("_git", "test.zwc", 0, 100).unwrap();
1831 cache
1832 .set_zstyle(":completion:*", "menu", &["select".to_string()], false)
1833 .unwrap();
1834 cache.set_comp("git", "_git").unwrap();
1835
1836 let stats = cache.stats().unwrap();
1837 assert_eq!(stats.autoloads, 1);
1838 assert_eq!(stats.zstyles, 1);
1839 assert_eq!(stats.comps, 1);
1840 }
1841
1842 #[test]
1843 fn test_large_scale() {
1844 let mut cache = CompsysCache::memory().unwrap();
1845
1846 let autoloads: Vec<(String, String, i64, i64)> = (0..10000)
1848 .map(|i| {
1849 (
1850 format!("_func{}", i),
1851 format!("src{}.zwc", i % 10),
1852 i * 50,
1853 50,
1854 )
1855 })
1856 .collect();
1857
1858 cache.add_autoloads_bulk(&autoloads).unwrap();
1859
1860 let stub = cache.get_autoload("_func9999").unwrap().unwrap();
1862 assert_eq!(stub.offset, 9999 * 50);
1863
1864 assert_eq!(cache.autoload_count().unwrap(), 10000);
1865 }
1866
1867 #[test]
1868 fn test_executables_cache() {
1869 let mut cache = CompsysCache::memory().unwrap();
1870
1871 let executables = vec![
1872 ("ls".to_string(), "/bin/ls".to_string()),
1873 ("cat".to_string(), "/bin/cat".to_string()),
1874 ("git".to_string(), "/usr/bin/git".to_string()),
1875 ];
1876 cache.set_executables_bulk(&executables).unwrap();
1877
1878 assert!(cache.has_executables().unwrap());
1879 assert!(cache.has_executable("ls").unwrap());
1880 assert!(cache.has_executable("git").unwrap());
1881 assert!(!cache.has_executable("nonexistent").unwrap());
1882
1883 assert_eq!(
1884 cache.get_executable_path("ls").unwrap(),
1885 Some("/bin/ls".to_string())
1886 );
1887 assert_eq!(cache.executables_count().unwrap(), 3);
1888 }
1889
1890 #[test]
1891 fn test_executables_prefix_search() {
1892 let mut cache = CompsysCache::memory().unwrap();
1893
1894 let executables = vec![
1895 ("git".to_string(), "/usr/bin/git".to_string()),
1896 ("gitk".to_string(), "/usr/bin/gitk".to_string()),
1897 ("grep".to_string(), "/bin/grep".to_string()),
1898 ("gzip".to_string(), "/bin/gzip".to_string()),
1899 ];
1900 cache.set_executables_bulk(&executables).unwrap();
1901
1902 let git_cmds = cache.get_executables_prefix_fts("git").unwrap();
1904 assert_eq!(git_cmds.len(), 2);
1905 assert!(git_cmds.iter().any(|(name, _)| name == "git"));
1906 assert!(git_cmds.iter().any(|(name, _)| name == "gitk"));
1907
1908 let g_cmds = cache.get_executables_prefix_fts("g").unwrap();
1909 assert_eq!(g_cmds.len(), 4);
1910 }
1911
1912 #[test]
1913 fn test_named_dirs_cache() {
1914 let mut cache = CompsysCache::memory().unwrap();
1915
1916 let dirs = vec![
1917 ("proj".to_string(), "/home/user/projects".to_string()),
1918 ("docs".to_string(), "/home/user/documents".to_string()),
1919 ];
1920 cache.set_named_dirs_bulk(&dirs).unwrap();
1921
1922 assert!(cache.has_named_dirs().unwrap());
1923
1924 let all = cache.get_named_dirs().unwrap();
1925 assert_eq!(all.len(), 2);
1926
1927 let p_dirs = cache.get_named_dirs_prefix("p").unwrap();
1928 assert_eq!(p_dirs.len(), 1);
1929 assert_eq!(p_dirs[0].0, "proj");
1930 }
1931
1932 #[test]
1933 fn test_shell_functions_cache() {
1934 let mut cache = CompsysCache::memory().unwrap();
1935
1936 let functions = vec![
1937 ("myFunc".to_string(), "/home/user/.zshrc".to_string()),
1938 (
1939 "zpwrClearList".to_string(),
1940 "/home/user/.zpwr/autoload".to_string(),
1941 ),
1942 (
1943 "zpwrTop".to_string(),
1944 "/home/user/.zpwr/autoload".to_string(),
1945 ),
1946 ];
1947 cache.set_shell_functions_bulk(&functions).unwrap();
1948
1949 assert!(cache.has_shell_functions().unwrap());
1950 assert_eq!(cache.shell_functions_count().unwrap(), 3);
1951
1952 let zpwr = cache.get_shell_functions_prefix("zpwr").unwrap();
1953 assert_eq!(zpwr.len(), 2);
1954 assert!(zpwr.iter().any(|(name, _)| name == "zpwrClearList"));
1956 assert!(zpwr.iter().any(|(name, _)| name == "zpwrTop"));
1957 }
1958
1959 #[test]
1960 fn test_metadata() {
1961 let cache = CompsysCache::memory().unwrap();
1962
1963 cache.set_metadata("version", "1.0.0").unwrap();
1964 cache.set_metadata("build_time", "2026-04-22").unwrap();
1965
1966 assert_eq!(
1967 cache.get_metadata("version").unwrap(),
1968 Some("1.0.0".to_string())
1969 );
1970 assert_eq!(
1971 cache.get_metadata("build_time").unwrap(),
1972 Some("2026-04-22".to_string())
1973 );
1974 assert_eq!(cache.get_metadata("nonexistent").unwrap(), None);
1975 }
1976
1977 #[test]
1978 fn test_comps_keys() {
1979 let mut cache = CompsysCache::memory().unwrap();
1980
1981 let comps = vec![
1982 ("git".to_string(), "_git".to_string()),
1983 ("docker".to_string(), "_docker".to_string()),
1984 ];
1985 cache.set_comps_bulk(&comps).unwrap();
1986
1987 let keys = cache.comps_keys().unwrap();
1988 assert_eq!(keys.len(), 2);
1989 assert!(keys.contains(&"docker".to_string()));
1990 assert!(keys.contains(&"git".to_string()));
1991 }
1992
1993 #[test]
1994 fn test_comps_prefix() {
1995 let mut cache = CompsysCache::memory().unwrap();
1996
1997 let comps = vec![
1998 ("git".to_string(), "_git".to_string()),
1999 ("gitk".to_string(), "_gitk".to_string()),
2000 ("docker".to_string(), "_docker".to_string()),
2001 ];
2002 cache.set_comps_bulk(&comps).unwrap();
2003
2004 let git_comps = cache.comps_prefix("git").unwrap();
2005 assert_eq!(git_comps.len(), 2);
2006 }
2007
2008 #[test]
2009 fn test_zstyles_bulk() {
2010 let mut cache = CompsysCache::memory().unwrap();
2011
2012 let styles = vec![
2013 (
2014 ":completion:*".to_string(),
2015 "menu".to_string(),
2016 vec!["select".to_string()],
2017 false,
2018 ),
2019 (
2020 ":completion:*".to_string(),
2021 "verbose".to_string(),
2022 vec!["yes".to_string()],
2023 false,
2024 ),
2025 (
2026 ":completion:*:descriptions".to_string(),
2027 "format".to_string(),
2028 vec!["%d".to_string()],
2029 false,
2030 ),
2031 ];
2032 cache.set_zstyles_bulk(&styles).unwrap();
2033
2034 assert!(cache.has_zstyles().unwrap());
2035 assert_eq!(cache.zstyles_count().unwrap(), 3);
2036 }
2037
2038 #[test]
2039 fn test_services() {
2040 let cache = CompsysCache::memory().unwrap();
2041
2042 cache.set_service("git", "scm").unwrap();
2043 cache.set_service("hg", "scm").unwrap();
2044
2045 assert_eq!(cache.get_service("git").unwrap(), Some("scm".to_string()));
2046 assert_eq!(cache.get_service("unknown").unwrap(), None);
2047 }
2048
2049 #[test]
2050 fn test_cache_overwrite() {
2051 let cache = CompsysCache::memory().unwrap();
2052
2053 cache.set_comp("git", "_git_old").unwrap();
2054 assert_eq!(cache.get_comp("git").unwrap(), Some("_git_old".to_string()));
2055
2056 cache.set_comp("git", "_git_new").unwrap();
2057 assert_eq!(cache.get_comp("git").unwrap(), Some("_git_new".to_string()));
2058 }
2059
2060 #[test]
2061 fn test_executable_names() {
2062 let mut cache = CompsysCache::memory().unwrap();
2063
2064 let executables = vec![
2065 ("alpha".to_string(), "/bin/alpha".to_string()),
2066 ("beta".to_string(), "/bin/beta".to_string()),
2067 ("gamma".to_string(), "/bin/gamma".to_string()),
2068 ];
2069 cache.set_executables_bulk(&executables).unwrap();
2070
2071 let names = cache.get_executable_names().unwrap();
2072 assert_eq!(names.len(), 3);
2073 assert!(names.contains("alpha"));
2075 assert!(names.contains("beta"));
2076 assert!(names.contains("gamma"));
2077 }
2078
2079 #[test]
2080 fn postpatcomps_do_not_land_in_patcomps() {
2081 let cache = CompsysCache::memory().unwrap();
2092 cache
2093 .set_patcomp("*/(init|rc[0-9S]#).d/*", "_init_d")
2094 .unwrap();
2095 cache
2096 .set_postpatcomp("-value-,*PATH,-default-", "_dir_list")
2097 .unwrap();
2098
2099 let pat = cache.patcomps_kv().unwrap();
2100 assert_eq!(
2101 pat,
2102 vec![("*/(init|rc[0-9S]#).d/*".to_string(), "_init_d".to_string())]
2103 );
2104
2105 let post = cache.postpatcomps_kv().unwrap();
2106 assert_eq!(
2107 post,
2108 vec![(
2109 "-value-,*PATH,-default-".to_string(),
2110 "_dir_list".to_string()
2111 )]
2112 );
2113 assert_eq!(cache.postpatcomps_count().unwrap(), 1);
2114 assert_eq!(cache.patcomps_count().unwrap(), 1);
2115 }
2116
2117 #[test]
2118 fn generation_one_completion_tables_are_rebuilt() {
2119 let mut cache = CompsysCache::memory().unwrap();
2124 cache
2125 .set_comps_bulk(&[("git".to_string(), "_git".to_string())])
2126 .unwrap();
2127 cache
2128 .set_patcomp("-value-,*PATH,-default-", "_dir_list")
2129 .unwrap();
2130 cache
2132 .conn
2133 .execute("DELETE FROM metadata WHERE key = 'completion_schema'", [])
2134 .unwrap();
2135
2136 cache.migrate_completion_tables().unwrap();
2137
2138 assert_eq!(
2139 cache.comp_count().unwrap(),
2140 0,
2141 "stale comps must be dropped"
2142 );
2143 assert_eq!(cache.patcomps_count().unwrap(), 0);
2144 assert_eq!(
2145 cache.get_metadata("completion_schema").unwrap().as_deref(),
2146 Some(CompsysCache::COMPLETION_SCHEMA_GENERATION)
2147 );
2148 }
2149}