From c1dabc988be8aa6b90bb93ebdcfe938ac67e66ea Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Sun, 31 May 2026 12:50:19 +0200 Subject: [PATCH] fix(store): accept lowercase drive letters in root_path integrity check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/store/store.c | 10 +++++-- tests/test_store_nodes.c | 63 +++++++++++++++++++--------------------- 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/src/store/store.c b/src/store/store.c index 16b28e8ef..bd738188f 100644 --- a/src/store/store.c +++ b/src/store/store.c @@ -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) { diff --git a/tests/test_store_nodes.c b/tests/test_store_nodes.c index cbcb9e727..0d84f4c78 100644 --- a/tests/test_store_nodes.c +++ b/tests/test_store_nodes.c @@ -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);