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
3 changes: 2 additions & 1 deletion .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ jobs:
test_db_storage_policies \
test_api_handlers_storage_targets \
test_api_handlers_storage_policies \
test_sqlite_migration_boundaries \
test_config \
test_logger \
test_strings \
Expand Down Expand Up @@ -146,7 +147,7 @@ jobs:
run: |
cd build
ctest --output-on-failure -V \
-R "test_storage_pressure$|test_storage_pressure_extended|test_detection_result_structures|test_stream_frame_rate|test_recording_video_params|test_mp4_segment_boundaries|test_storage_retention_sqlite|test_storage_manager_retention|test_storage_target_pressure_cleanup|test_storage_migration|test_storage_archive_s3|test_storage_archive_operator|test_authorization|test_api_handlers_recordings_playback|test_db_storage_targets|test_db_storage_policies|test_api_handlers_storage_targets|test_api_handlers_storage_policies|test_config|test_logger|test_strings|test_memory|test_request_response|test_shutdown_coordinator|test_detection_config|test_detection_model_motion|test_db_streams|test_db_camera_favorites|test_api_handlers_camera_favorites|test_db_backup|test_mp4_failed_open_memory|test_db_recordings_extended|test_db_detections|test_db_zones|test_db_events|test_db_auth|test_db_transactions|test_db_maintenance|test_db_query_builder|test_logger_json|test_batch_delete_progress|test_db_recordings_sync|test_httpd_utils|test_zone_filter|test_onvif_soap_fault|test_faac_encoder|test_audio_transcoder|test_mp4_audio_recovery|test_mp4_forced_reconnect|test_mp4_recovery_watchdog|test_audio_voice_enhancement|test_audio_voice_filter_faults|test_udt_recording|test_detection_current_frame|test_detection_io_recovery|test_hls_cleanup|test_stream_manager|test_stream_state|test_packet_buffer|test_timestamp_manager|test_motion_trigger_parse|test_motion_max_area|test_url_utils|test_system_health_startup$"
-R "test_sqlite_migration_boundaries|test_storage_pressure$|test_storage_pressure_extended|test_detection_result_structures|test_stream_frame_rate|test_recording_video_params|test_mp4_segment_boundaries|test_storage_retention_sqlite|test_storage_manager_retention|test_storage_target_pressure_cleanup|test_storage_migration|test_storage_archive_s3|test_storage_archive_operator|test_authorization|test_api_handlers_recordings_playback|test_db_storage_targets|test_db_storage_policies|test_api_handlers_storage_targets|test_api_handlers_storage_policies|test_config|test_logger|test_strings|test_memory|test_request_response|test_shutdown_coordinator|test_detection_config|test_detection_model_motion|test_db_streams|test_db_camera_favorites|test_api_handlers_camera_favorites|test_db_backup|test_mp4_failed_open_memory|test_db_recordings_extended|test_db_detections|test_db_zones|test_db_events|test_db_auth|test_db_transactions|test_db_maintenance|test_db_query_builder|test_logger_json|test_batch_delete_progress|test_db_recordings_sync|test_httpd_utils|test_zone_filter|test_onvif_soap_fault|test_faac_encoder|test_audio_transcoder|test_mp4_audio_recovery|test_mp4_forced_reconnect|test_mp4_recovery_watchdog|test_audio_voice_enhancement|test_audio_voice_filter_faults|test_udt_recording|test_detection_current_frame|test_detection_io_recovery|test_hls_cleanup|test_stream_manager|test_stream_state|test_packet_buffer|test_timestamp_manager|test_motion_trigger_parse|test_motion_max_area|test_url_utils|test_system_health_startup$"

- name: Generate coverage report
if: always()
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/sanitizer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jobs:
cd build
echo "Running tests with ASan + UBSan enabled..."
ctest --output-on-failure -V \
-R "test_config|test_logger|test_strings|test_memory|test_request_response|test_shutdown_coordinator|test_detection_config|test_detection_model_motion|test_db_streams|test_db_recordings_extended|test_db_detections|test_db_zones|test_db_events|test_db_auth|test_db_transactions|test_db_maintenance|test_db_query_builder|test_logger_json|test_batch_delete_progress|test_db_recordings_sync|test_httpd_utils|test_zone_filter|test_faac_encoder|test_audio_transcoder|test_audio_voice_enhancement|test_audio_voice_filter_faults|test_udt_recording|test_detection_current_frame|test_detection_io_recovery|test_stream_manager|test_stream_state|test_packet_buffer|test_timestamp_manager|test_storage_pressure$|test_storage_pressure_extended|test_detection_result_structures|test_stream_frame_rate|test_recording_video_params|test_mp4_segment_boundaries|test_storage_retention_sqlite|test_audit_log|test_audit_summary"
-R "test_sqlite_migration_boundaries|test_config|test_logger|test_strings|test_memory|test_request_response|test_shutdown_coordinator|test_detection_config|test_detection_model_motion|test_db_streams|test_db_recordings_extended|test_db_detections|test_db_zones|test_db_events|test_db_auth|test_db_transactions|test_db_maintenance|test_db_query_builder|test_logger_json|test_batch_delete_progress|test_db_recordings_sync|test_httpd_utils|test_zone_filter|test_faac_encoder|test_audio_transcoder|test_audio_voice_enhancement|test_audio_voice_filter_faults|test_udt_recording|test_detection_current_frame|test_detection_io_recovery|test_stream_manager|test_stream_state|test_packet_buffer|test_timestamp_manager|test_storage_pressure$|test_storage_pressure_extended|test_detection_result_structures|test_stream_frame_rate|test_recording_video_params|test_mp4_segment_boundaries|test_storage_retention_sqlite|test_audit_log|test_audit_summary"

- name: Smoke test - start and stop service
run: |
Expand Down
87 changes: 54 additions & 33 deletions src/database/sqlite_migrate.c
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,16 @@ static int create_migrations_table(sqlite_migrate_t *ctx) {
const char *table_name = ctx->config.migrations_table ?
ctx->config.migrations_table : DEFAULT_MIGRATIONS_TABLE;

char sql[512];
snprintf(sql, sizeof(sql),
"CREATE TABLE IF NOT EXISTS %s ("
char *sql = sqlite3_mprintf(
"CREATE TABLE IF NOT EXISTS \"%w\" ("
" version TEXT PRIMARY KEY,"
" applied_at INTEGER NOT NULL DEFAULT (strftime('%%s', 'now'))"
");", table_name);
if (!sql) return -1;

char *err_msg = NULL;
int rc = sqlite3_exec(ctx->db, sql, NULL, NULL, &err_msg);
sqlite3_free(sql);
if (rc != SQLITE_OK) {
log_error("Failed to create migrations table: %s", err_msg);
sqlite3_free(err_msg);
Expand All @@ -72,12 +73,13 @@ static bool is_migration_applied(sqlite_migrate_t *ctx, const char *version) {
const char *table_name = ctx->config.migrations_table ?
ctx->config.migrations_table : DEFAULT_MIGRATIONS_TABLE;

char sql[256];
snprintf(sql, sizeof(sql),
"SELECT 1 FROM %s WHERE version = ?;", table_name);
char *sql = sqlite3_mprintf(
"SELECT 1 FROM \"%w\" WHERE version = ?;", table_name);
if (!sql) return false;

sqlite3_stmt *stmt;
int rc = sqlite3_prepare_v2(ctx->db, sql, -1, &stmt, NULL);
sqlite3_free(sql);
if (rc != SQLITE_OK) {
return false;
}
Expand All @@ -97,12 +99,13 @@ static int record_migration_applied(sqlite_migrate_t *ctx, const char *version)
const char *table_name = ctx->config.migrations_table ?
ctx->config.migrations_table : DEFAULT_MIGRATIONS_TABLE;

char sql[256];
snprintf(sql, sizeof(sql),
"INSERT INTO %s (version) VALUES (?);", table_name);
char *sql = sqlite3_mprintf(
"INSERT INTO \"%w\" (version) VALUES (?);", table_name);
if (!sql) return -1;

sqlite3_stmt *stmt;
int rc = sqlite3_prepare_v2(ctx->db, sql, -1, &stmt, NULL);
sqlite3_free(sql);
if (rc != SQLITE_OK) {
log_error("Failed to prepare migration record: %s", sqlite3_errmsg(ctx->db));
return -1;
Expand All @@ -128,12 +131,13 @@ static int remove_migration_record(sqlite_migrate_t *ctx, const char *version) {
const char *table_name = ctx->config.migrations_table ?
ctx->config.migrations_table : DEFAULT_MIGRATIONS_TABLE;

char sql[256];
snprintf(sql, sizeof(sql),
"DELETE FROM %s WHERE version = ?;", table_name);
char *sql = sqlite3_mprintf(
"DELETE FROM \"%w\" WHERE version = ?;", table_name);
if (!sql) return -1;

sqlite3_stmt *stmt;
int rc = sqlite3_prepare_v2(ctx->db, sql, -1, &stmt, NULL);
sqlite3_free(sql);
if (rc != SQLITE_OK) {
return -1;
}
Expand Down Expand Up @@ -234,11 +238,12 @@ static int validate_migration_file_security(const char *filepath) {
* A CREATE TRIGGER body is a sequence of statements wrapped in BEGIN..END, so
* splitting on the first semicolon would cut it in half: the allowlist would
* reject the dangling END and sqlite3_exec would report "incomplete input".
* Treat the whole trigger, including nested CASE..END expressions, as one
* statement. Single-quoted literals are skipped everywhere.
* As in SQLite's complete.c, a trigger ends with the token sequence ; END ;.
* Counting BEGIN/CASE is incorrect because they can also be identifier names.
* Quoted literals/identifiers and comments are skipped everywhere.
*/
static bool is_word_char(char c) {
return isalnum((unsigned char)c) || c == '_';
return isalnum((unsigned char)c) || c == '_' || c == '$' || (unsigned char)c >= 0x80;
}

/* Match `word` at `cursor` on identifier boundaries, so a column such as
Expand All @@ -261,33 +266,49 @@ static const char *statement_end(const char *start) {
trigger = word_matches(start, after, "TRIGGER", 7);
}

int in_string = 0;
int depth = 0; /* BEGIN/CASE blocks still open */
bool body_started = false;
char quote = 0;
unsigned terminator = 0; /* 0: other token, 1: semicolon, 2: semicolon END */
for (cursor = start; *cursor; cursor++) {
if (*cursor == '\'') {
in_string = !in_string;
if (quote) {
if (*cursor == quote) {
/* SQLite doubles quote delimiters inside strings/identifiers;
* bracket identifiers instead end at the first closing ]. */
if (quote != ']' && cursor[1] == quote) cursor++;
else quote = 0;
}
continue;
}
if (*cursor == '\'' || *cursor == '"' || *cursor == '`' || *cursor == '[') {
terminator = 0;
quote = *cursor == '[' ? ']' : *cursor;
continue;
}
if (cursor[0] == '-' && cursor[1] == '-') {
while (*cursor && *cursor != '\n') cursor++;
if (!*cursor) return cursor;
continue;
}
if (cursor[0] == '/' && cursor[1] == '*') {
cursor += 2;
while (*cursor && !(cursor[0] == '*' && cursor[1] == '/')) cursor++;
if (!*cursor) return cursor;
cursor++;
continue;
}
if (in_string) continue;
if (!trigger) {
if (*cursor == ';') return cursor;
continue;
}
if (word_matches(start, cursor, "BEGIN", 5) ||
word_matches(start, cursor, "CASE", 4)) {
depth++;
body_started = true;
if (isspace((unsigned char)*cursor)) continue;
if (*cursor == ';') {
if (terminator == 2) return cursor;
terminator = 1;
continue;
}
if (word_matches(start, cursor, "END", 3)) {
if (depth > 0) depth--;
if (body_started && depth == 0) {
cursor += 3;
while (*cursor && isspace((unsigned char)*cursor)) cursor++;
return cursor;
}
}
if (terminator == 1 && word_matches(start, cursor, "END", 3)) {
terminator = 2;
cursor += 2;
} else terminator = 0;
}
return cursor;
}
Expand Down
2 changes: 2 additions & 0 deletions tests/unit/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ target_sources(test_stream_frame_rate PRIVATE
# ====================================================================
add_layer2_test(test_config)
add_layer2_test(test_db_core_initialization)
add_layer2_test(test_sqlite_migration_boundaries)
set_tests_properties(test_sqlite_migration_boundaries PROPERTIES TIMEOUT 20)
add_layer2_test(test_logger)
add_layer2_test(test_strings)
add_layer2_test(test_interruptible_sleep)
Expand Down
Loading
Loading