Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 6 additions & 9 deletions src/cli/cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "foundation/constants.h"
#include "foundation/log.h"
#include "foundation/sha256.h"
#include "foundation/str_util.h"
#include "cli/client_adapter.h"
#include "mcp/mcp.h" // cbm_mcp_tool_input_schema — CLI flag parser + per-tool --help
#include "mcp/index_supervisor.h"
Expand Down Expand Up @@ -62,7 +63,6 @@ enum {
SQL_PARAM_1 = 1, /* sqlite3_bind parameter index 1 */
SQL_PARAM_2 = 2,
SEMVER_PARTS = 3, /* major.minor.patch */
DB_EXT_LEN = 3, /* strlen(".db") */
MIN_ARGC_CMD = 3,
/* minimum argc for subcommand with arg */ /* sqlite3_bind parameter index 2 */ /* 10 MB cap
factor */
Expand Down Expand Up @@ -7207,8 +7207,7 @@ int cbm_list_indexes(const char *home_dir) {
int count = 0;
cbm_dirent_t *ent;
while ((ent = cbm_readdir(d)) != NULL) {
size_t len = strlen(ent->name);
if (len > DB_EXT_LEN && strcmp(ent->name + len - DB_EXT_LEN, ".db") == 0) {
if (cbm_is_project_index_db(ent->name)) {
printf(" %s/%s\n", cache_dir, ent->name);
count++;
}
Expand All @@ -7231,8 +7230,7 @@ int cbm_remove_indexes(const char *home_dir) {
int count = 0;
cbm_dirent_t *ent;
while ((ent = cbm_readdir(d)) != NULL) {
size_t len = strlen(ent->name);
if (len > DB_EXT_LEN && strcmp(ent->name + len - DB_EXT_LEN, ".db") == 0) {
if (cbm_is_project_index_db(ent->name)) {
char path[CLI_BUF_1K];
snprintf(path, sizeof(path), "%s/%s", cache_dir, ent->name);
/* Also remove .db.tmp if present */
Expand Down Expand Up @@ -7268,7 +7266,7 @@ cbm_config_t *cbm_config_open(const char *cache_dir) {
}

char dbpath[CLI_BUF_1K];
snprintf(dbpath, sizeof(dbpath), "%s/_config.db", cache_dir);
snprintf(dbpath, sizeof(dbpath), "%s/" CBM_CONFIG_DB_FILENAME, cache_dir);

/* Ensure directory exists */
mkdirp(cache_dir, DIR_PERMS);
Expand Down Expand Up @@ -10423,7 +10421,7 @@ static int cbm_install_agent_configs_with_previous(const char *home, const char
return result;
}

/* Count .db files in the cache directory. */
/* Count project index .db files in the cache directory (internal stores excluded). */
static int count_db_indexes(const char *home) {
const char *cache_dir = get_cache_dir(home);
if (!cache_dir) {
Expand All @@ -10436,8 +10434,7 @@ static int count_db_indexes(const char *home) {
int count = 0;
cbm_dirent_t *ent;
while ((ent = cbm_readdir(d)) != NULL) {
size_t len = strlen(ent->name);
if (len > DB_EXT_LEN && strcmp(ent->name + len - DB_EXT_LEN, ".db") == 0) {
if (cbm_is_project_index_db(ent->name)) {
count++;
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/cli/cli.h
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,8 @@ unsigned char *cbm_extract_binary_from_zip(const unsigned char *data, int data_l
* Prints each file path to stdout. Returns count of .db files found. */
int cbm_list_indexes(const char *home_dir);

/* Remove all .db files in the cache directory. Returns count removed. */
/* Remove every project index .db (and its sidecars) in the cache directory.
* Internal stores (_config.db, _cross_repo.db) are kept. Returns count removed. */
int cbm_remove_indexes(const char *home_dir);

/* ── Config store (persistent key-value, backed by _config.db) ── */
Expand Down
18 changes: 18 additions & 0 deletions src/foundation/str_util.c
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,24 @@ bool cbm_validate_project_name(const char *name) {
return true;
}

bool cbm_is_internal_cache_db(const char *filename) {
return filename && (strcmp(filename, CBM_CONFIG_DB_FILENAME) == 0 ||
strcmp(filename, CBM_CROSS_REPO_DB_FILENAME) == 0);
}

bool cbm_is_project_index_db(const char *filename) {
static const char db_ext[] = ".db";
const size_t ext_len = sizeof(db_ext) - 1;
if (!filename) {
return false;
}
size_t len = strlen(filename);
if (len <= ext_len || strcmp(filename + len - ext_len, db_ext) != 0) {
return false;
}
return !cbm_is_internal_cache_db(filename);
}

int cbm_utf8_trim_partial(char *buf) {
if (!buf) {
return 0;
Expand Down
16 changes: 16 additions & 0 deletions src/foundation/str_util.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,22 @@ bool cbm_validate_shell_path_arg(const char *path);
* Returns true if safe, false if the name could escape the cache directory. */
bool cbm_validate_project_name(const char *name);

/* Internal stores that live next to the project indexes in the cache
* directory. They are exact filenames: a project name may itself begin with
* "_" or contain "config", so prefix/substring filters would hide real
* projects. */
#define CBM_CONFIG_DB_FILENAME "_config.db"
#define CBM_CROSS_REPO_DB_FILENAME "_cross_repo.db"

/* True when a cache-directory entry is one of the internal stores above. */
bool cbm_is_internal_cache_db(const char *filename);

/* True when a cache-directory entry is a project index: "<name>.db" with a
* non-empty stem and not an internal store. The one predicate every
* enumeration of the cache directory's .db files uses (list, count,
* remove, cross-repo). */
bool cbm_is_project_index_db(const char *filename);

/* Safe snprintf append: clamps offset to prevent buffer overflow on truncation.
* When snprintf truncates, it returns what it WOULD have written, which can make
* offset > bufsize. Next call: bufsize - offset wraps unsigned → huge → overflow.
Expand Down
12 changes: 5 additions & 7 deletions src/pipeline/pass_cross_repo.c
Original file line number Diff line number Diff line change
Expand Up @@ -1094,15 +1094,13 @@ static int collect_all_projects(char ***out, cr_run_context_t *ctx) {
failed = true;
break;
}
size_t len = strlen(ent->name);
if (len < CR_COL_4 || strcmp(ent->name + len - CR_DB_EXT_LEN, ".db") != 0) {
continue;
}
/* Internal stores are exact filenames. Substring filtering would hide
* legitimate projects such as orders_config_service or api-wal. */
if (strcmp(ent->name, "_cross_repo.db") == 0 || strcmp(ent->name, "_config.db") == 0) {
/* Internal stores are exact filenames (cbm_is_internal_cache_db).
* Substring filtering would hide legitimate projects such as
* orders_config_service or api-wal. */
if (!cbm_is_project_index_db(ent->name)) {
continue;
}
size_t len = strlen(ent->name);
if (count >= CR_MAX_PROJECTS) {
failed = true;
break;
Expand Down
66 changes: 66 additions & 0 deletions tests/test_cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -2285,6 +2285,71 @@ TEST(cli_remove_indexes_deletes_orphan_sqlite_sidecars_issue2054) {
PASS();
}

/* The cache directory also holds internal stores next to the project
* indexes: the user's settings (_config.db) and the cross-repo store
* (_cross_repo.db). Index enumeration treated every *.db as a project, so
* install --reset-indexes / uninstall deleted the user's config along with
* the indexes, and list/count over-reported. Internal stores are exact
* filenames: a real project whose name starts with "_" is still an index. */
TEST(cli_index_enumeration_skips_internal_cache_dbs) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-internal-dbs-XXXXXX");
if (!cbm_mkdtemp(tmpdir)) {
FAIL("cbm_mkdtemp failed");
}
char *old_home = NULL;
char *old_cache = NULL;
cli_activation_save_env(&old_home, &old_cache);
cbm_setenv("HOME", tmpdir, 1);
char cache_dir[512];
snprintf(cache_dir, sizeof(cache_dir), "%s/cache", tmpdir);
cbm_setenv("CBM_CACHE_DIR", cache_dir, 1);
test_mkdirp(cache_dir);

cbm_config_t *cfg = cbm_config_open(cache_dir);
int set_rc = cfg ? cbm_config_set(cfg, "auto_index", "true") : -1;
cbm_config_close(cfg);

char config_path[640];
char cross_path[640];
char proj_path[640];
char underscore_proj_path[640];
snprintf(config_path, sizeof(config_path), "%s/_config.db", cache_dir);
snprintf(cross_path, sizeof(cross_path), "%s/_cross_repo.db", cache_dir);
snprintf(proj_path, sizeof(proj_path), "%s/proj.db", cache_dir);
snprintf(underscore_proj_path, sizeof(underscore_proj_path), "%s/_work-api.db", cache_dir);
write_test_file(cross_path, "cross");
write_test_file(proj_path, "db");
write_test_file(underscore_proj_path, "db");

int listed = cbm_list_indexes(tmpdir);
int removed = cbm_remove_indexes(tmpdir);

struct stat st;
bool config_kept = stat(config_path, &st) == 0;
bool cross_kept = stat(cross_path, &st) == 0;
bool proj_absent = stat(proj_path, &st) != 0;
bool underscore_proj_absent = stat(underscore_proj_path, &st) != 0;
char value[32] = "";
cfg = cbm_config_open(cache_dir);
if (cfg) {
snprintf(value, sizeof(value), "%s", cbm_config_get(cfg, "auto_index", ""));
cbm_config_close(cfg);
}
cli_activation_restore_env(old_home, old_cache);
test_rmdir_r(tmpdir);

ASSERT_EQ(set_rc, 0);
ASSERT_EQ(listed, 2);
ASSERT_EQ(removed, 2);
ASSERT_TRUE(config_kept);
ASSERT_TRUE(cross_kept);
ASSERT_TRUE(proj_absent);
ASSERT_TRUE(underscore_proj_absent);
ASSERT_STR_EQ(value, "true");
PASS();
}

TEST(cli_install_config_only_waits_for_cohort_drain) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-daemon-install-config-race-XXXXXX");
Expand Down Expand Up @@ -16264,6 +16329,7 @@ SUITE(cli) {
RUN_TEST(cli_activation_commands_reject_malformed_and_unknown_flags);
RUN_TEST(cli_install_reset_deletion_waits_for_final_activation_guard);
RUN_TEST(cli_remove_indexes_deletes_orphan_sqlite_sidecars_issue2054);
RUN_TEST(cli_index_enumeration_skips_internal_cache_dbs);
RUN_TEST(cli_install_config_only_waits_for_cohort_drain);
RUN_TEST(cli_install_config_and_path_finish_before_guard_release);
RUN_TEST(cli_install_config_failure_keeps_published_binary);
Expand Down
Loading