fix(store): accept lowercase drive letters in root_path integrity check

The projects.root_path integrity check only accepted '/' or an uppercase
'A'-'Z' first character. On Windows, drive letters are commonly lowercase
(c:/repo, y:/share), so such a path was flagged store.corrupt and the DB
was auto-deleted on open — a likely cause of the mapped-drive DB deletion
in #227/#367. Accept 'a'-'z' as well.

Relates to #227, #367.
This commit is contained in:
Martin Vogel
2026-05-31 12:50:19 +02:00
parent 684045708f
commit c1dabc988b
2 changed files with 37 additions and 36 deletions
+7 -3
View File
@@ -684,12 +684,16 @@ bool cbm_store_check_integrity(cbm_store_t *s) {
sqlite3_finalize(stmt);
if (ok) {
/* Check that root_path in projects table starts with '/' or a drive letter.
* Corrupt DBs often have numeric strings like "826" in root_path. */
/* Check that root_path in projects table starts with '/' or a drive
* letter. Corrupt DBs often have numeric strings like "826" in
* root_path. Drive letters may be upper- OR lower-case on Windows
* (e.g. "c:/repo", "y:/share") — rejecting lowercase here flagged
* valid Windows paths as corrupt and deleted the DB (#227/#367). */
rc = sqlite3_prepare_v2(s->db,
"SELECT root_path FROM projects WHERE root_path != '' "
"AND NOT (substr(root_path, 1, 1) = '/' "
"OR (substr(root_path, 1, 1) BETWEEN 'A' AND 'Z')) LIMIT 1;",
"OR (substr(root_path, 1, 1) BETWEEN 'A' AND 'Z') "
"OR (substr(root_path, 1, 1) BETWEEN 'a' AND 'z')) LIMIT 1;",
CBM_NOT_FOUND, &stmt, NULL);
if (rc == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) {
+30 -33
View File
@@ -974,6 +974,18 @@ TEST(store_integrity_corrupt_bad_path) {
PASS();
}
TEST(store_integrity_windows_lowercase_drive_issue367) {
/* Windows drive letters may be lower- or upper-case; a lowercase drive
* path must NOT be treated as corrupt. Previously the check only accepted
* 'A'..'Z', so "c:/repo" was flagged and the DB auto-deleted (#227/#367). */
cbm_store_t *s = cbm_store_open_memory();
ASSERT_NOT_NULL(s);
cbm_store_upsert_project(s, "lc-drive", "c:/Users/dev/repo");
ASSERT_TRUE(cbm_store_check_integrity(s));
cbm_store_close(s);
PASS();
}
TEST(store_integrity_corrupt_too_many_rows) {
/* Simulate corruption: >5 rows in projects table */
cbm_store_t *s = cbm_store_open_memory();
@@ -1005,10 +1017,8 @@ TEST(store_node_null_project) {
ASSERT_NOT_NULL(s);
/* Upsert with NULL project — should fail gracefully */
cbm_node_t n = {.project = NULL,
.label = "Function",
.name = "Foo",
.qualified_name = "null.Foo"};
cbm_node_t n = {
.project = NULL, .label = "Function", .name = "Foo", .qualified_name = "null.Foo"};
int64_t id = cbm_store_upsert_node(s, &n);
/* Either returns error or silently succeeds; must not crash */
(void)id;
@@ -1022,10 +1032,7 @@ TEST(store_node_null_qn) {
cbm_store_upsert_project(s, "test", "/tmp/test");
/* Upsert with NULL qualified_name */
cbm_node_t n = {.project = "test",
.label = "Function",
.name = "Bar",
.qualified_name = NULL};
cbm_node_t n = {.project = "test", .label = "Function", .name = "Bar", .qualified_name = NULL};
int64_t id = cbm_store_upsert_node(s, &n);
/* Must not crash regardless of return value */
(void)id;
@@ -1085,10 +1092,8 @@ TEST(store_find_by_qn_not_found) {
cbm_store_upsert_project(s, "test", "/tmp/test");
/* Insert a node so the store is non-empty */
cbm_node_t n = {.project = "test",
.label = "Function",
.name = "Exists",
.qualified_name = "test.Exists"};
cbm_node_t n = {
.project = "test", .label = "Function", .name = "Exists", .qualified_name = "test.Exists"};
cbm_store_upsert_node(s, &n);
/* Search for a non-existent QN */
@@ -1284,22 +1289,14 @@ TEST(store_delete_by_label_verify_remaining) {
cbm_store_t *s = cbm_store_open_memory();
cbm_store_upsert_project(s, "test", "/tmp/test");
cbm_node_t n1 = {.project = "test",
.label = "Function",
.name = "FuncA",
.qualified_name = "test.FuncA"};
cbm_node_t n2 = {.project = "test",
.label = "Class",
.name = "ClassB",
.qualified_name = "test.ClassB"};
cbm_node_t n3 = {.project = "test",
.label = "Function",
.name = "FuncC",
.qualified_name = "test.FuncC"};
cbm_node_t n4 = {.project = "test",
.label = "Method",
.name = "MethodD",
.qualified_name = "test.MethodD"};
cbm_node_t n1 = {
.project = "test", .label = "Function", .name = "FuncA", .qualified_name = "test.FuncA"};
cbm_node_t n2 = {
.project = "test", .label = "Class", .name = "ClassB", .qualified_name = "test.ClassB"};
cbm_node_t n3 = {
.project = "test", .label = "Function", .name = "FuncC", .qualified_name = "test.FuncC"};
cbm_node_t n4 = {
.project = "test", .label = "Method", .name = "MethodD", .qualified_name = "test.MethodD"};
cbm_store_upsert_node(s, &n1);
cbm_store_upsert_node(s, &n2);
cbm_store_upsert_node(s, &n3);
@@ -1471,11 +1468,10 @@ TEST(store_node_properties_special_chars) {
cbm_store_upsert_project(s, "test", "/tmp/test");
/* JSON with quotes, backslashes, unicode, newlines */
const char *props =
"{\"desc\":\"line1\\nline2\","
"\"path\":\"C:\\\\Users\\\\test\","
"\"emoji\":\"\\u2603\","
"\"nested\":{\"key\":\"val with \\\"quotes\\\"\"}}";
const char *props = "{\"desc\":\"line1\\nline2\","
"\"path\":\"C:\\\\Users\\\\test\","
"\"emoji\":\"\\u2603\","
"\"nested\":{\"key\":\"val with \\\"quotes\\\"\"}}";
cbm_node_t n = {.project = "test",
.label = "Function",
@@ -1548,6 +1544,7 @@ SUITE(store_nodes) {
RUN_TEST(store_integrity_clean);
RUN_TEST(store_integrity_empty);
RUN_TEST(store_integrity_corrupt_bad_path);
RUN_TEST(store_integrity_windows_lowercase_drive_issue367);
RUN_TEST(store_integrity_corrupt_too_many_rows);
RUN_TEST(store_integrity_null_check);
RUN_TEST(store_project_crud);