| 677 | } |
| 678 | |
| 679 | static cbm_store_t *store_open_internal(const char *path, bool in_memory, bool create) { |
| 680 | cbm_store_t *s = calloc(CBM_ALLOC_ONE, sizeof(cbm_store_t)); |
| 681 | if (!s) { |
| 682 | return NULL; |
| 683 | } |
| 684 | |
| 685 | int flags = SQLITE_OPEN_READWRITE | (create ? SQLITE_OPEN_CREATE : 0); |
| 686 | if (in_memory) { |
| 687 | flags |= SQLITE_OPEN_MEMORY; |
| 688 | } |
| 689 | |
| 690 | char open_path[4096]; |
| 691 | const char *effective_path = path; |
| 692 | if (path && !in_memory) { |
| 693 | if (!cbm_path_for_file_api(path, open_path, sizeof(open_path))) { |
| 694 | free(s); |
| 695 | return NULL; |
| 696 | } |
| 697 | effective_path = open_path; |
| 698 | } |
| 699 | int rc = sqlite3_open_v2(effective_path, &s->db, flags, NULL); |
| 700 | if (rc != SQLITE_OK) { |
| 701 | sqlite3_close(s->db); |
| 702 | free(s); |
| 703 | return NULL; |
| 704 | } |
| 705 | |
| 706 | if (path && !in_memory) { |
| 707 | s->db_path = heap_strdup(path); |
| 708 | } |
| 709 | |
| 710 | /* Security: block ATTACH/DETACH to prevent file creation via SQL injection. |
| 711 | * The authorizer runs inside SQLite's query planner — no string-level bypass. */ |
| 712 | sqlite3_set_authorizer(s->db, store_authorizer, NULL); |
| 713 | |
| 714 | /* Register REGEXP function (SQLite doesn't have one built-in) */ |
| 715 | sqlite3_create_function(s->db, "regexp", ST_COL_2, SQLITE_UTF8 | SQLITE_DETERMINISTIC, NULL, |
| 716 | sqlite_regexp, NULL, NULL); |
| 717 | /* Case-insensitive variant for search with case_sensitive=false */ |
| 718 | sqlite3_create_function(s->db, "iregexp", ST_COL_2, SQLITE_UTF8 | SQLITE_DETERMINISTIC, NULL, |
| 719 | sqlite_iregexp, NULL, NULL); |
| 720 | /* Int8 cosine similarity for vector search */ |
| 721 | sqlite3_create_function(s->db, "cbm_cosine_i8", ST_COL_2, SQLITE_UTF8 | SQLITE_DETERMINISTIC, |
| 722 | NULL, sqlite_cosine_i8, NULL, NULL); |
| 723 | /* camelCase splitter for FTS5 BM25 indexing */ |
| 724 | sqlite3_create_function(s->db, "cbm_camel_split", SKIP_ONE, SQLITE_UTF8 | SQLITE_DETERMINISTIC, |
| 725 | NULL, sqlite_camel_split, NULL, NULL); |
| 726 | |
| 727 | if (configure_pragmas(s, in_memory, false) != CBM_STORE_OK || init_schema(s) != CBM_STORE_OK || |
| 728 | create_user_indexes(s) != CBM_STORE_OK) { |
| 729 | sqlite3_close(s->db); |
| 730 | safe_str_free(&s->db_path); |
| 731 | free(s); |
| 732 | return NULL; |
| 733 | } |
| 734 | |
| 735 | return s; |
| 736 | } |
no test coverage detected