/****************************************************************************** * @file database.c * @brief Initialisation de la base SQLite d'une enquête. ******************************************************************************/ #include "database/database.h" #include "database/schema.h" #include "database/transaction.h" #include "database/statement.h" #include "database_internal.h" #include #include /** * @brief Version actuelle du schéma SQLite. */ #define DATABASE_SCHEMA_VERSION_CURRENT 8 /** * @brief Version actuelle sous forme textuelle pour metadata. */ #define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "8" /** * @brief Nom de l'application enregistré dans les métadonnées. */ #define DATABASE_APPLICATION_NAME "Labfy Investigation" struct Database { sqlite3 *handle; char *database_path; bool transaction_active; int schema_version; DatabaseErrorCode error_code; char *error_message; }; /** * @brief Requête d'insertion d'une métadonnée. */ static const char *const database_insert_metadata_sql = "INSERT INTO metadata (key, value) VALUES (?, ?);"; /** * @brief Requête d'insertion de l'enquête. */ static const char *const database_insert_investigation_sql = "INSERT INTO investigation" "(" " id," " name," " root_path," " created_at," " updated_at" ")" "VALUES" "(" " ?," " ?," " ?," " ?," " ?" ");"; /** * @brief Vérifie les paramètres publics de l'initialisation. */ static bool database_validate_initialize_parameters( const char *database_path, const char *investigation_name, const char *investigation_root_path ) { if (database_path == NULL || database_path[0] == '\0') { return false; } if (investigation_name == NULL || investigation_name[0] == '\0') { return false; } if (investigation_root_path == NULL || investigation_root_path[0] == '\0') { return false; } return true; } /** * @brief Exécute une requête SQL statique. * * Cette fonction convient uniquement aux requêtes qui ne contiennent * aucune donnée provenant de l'utilisateur. */ static bool database_execute_sql( sqlite3 *database, const char *sql ) { char *error_message = NULL; int result = SQLITE_ERROR; if (database == NULL || sql == NULL) { return false; } result = sqlite3_exec( database, sql, NULL, NULL, &error_message ); if (result != SQLITE_OK) { g_warning( "Erreur SQLite : %s", error_message != NULL ? error_message : sqlite3_errmsg(database) ); sqlite3_free(error_message); return false; } sqlite3_free(error_message); return true; } /** * @brief Insère une paire clé-valeur avec une requête préparée. * * La requête est réinitialisée après chaque insertion afin de pouvoir * être réutilisée. */ static bool database_insert_metadata( DatabaseStatement *statement, const char *key, const char *value ) { if (statement == NULL || key == NULL || value == NULL) { return false; } if (!database_statement_bind_text( statement, 1, key )) { return false; } if (!database_statement_bind_text( statement, 2, value )) { return false; } if (database_statement_step(statement) != DATABASE_STATEMENT_STEP_DONE) { return false; } if (!database_statement_reset(statement)) { return false; } if (!database_statement_clear_bindings(statement)) { return false; } return true; } /** * @brief Insère toutes les métadonnées obligatoires. */ static bool database_insert_all_metadata( Database *database, const char *created_at, const char *investigation_uuid ) { DatabaseStatement *statement = NULL; bool success = false; if (database == NULL || created_at == NULL || investigation_uuid == NULL) { return false; } statement = database_statement_prepare( database, database_insert_metadata_sql ); if (statement == NULL) { return false; } success = database_insert_metadata( statement, "schema_version", DATABASE_SCHEMA_VERSION_CURRENT_TEXT ) && database_insert_metadata( statement, "application", DATABASE_APPLICATION_NAME ) && database_insert_metadata( statement, "created_at", created_at ) && database_insert_metadata( statement, "investigation_uuid", investigation_uuid ); database_statement_finalize(statement); return success; } /** * @brief Insère la ligne représentant l'enquête courante. */ static bool database_insert_investigation( Database *database, const char *investigation_uuid, const char *investigation_name, const char *investigation_root_path, const char *created_at ) { DatabaseStatement *statement = NULL; bool success = false; if (database == NULL || investigation_uuid == NULL || investigation_name == NULL || investigation_root_path == NULL || created_at == NULL) { return false; } statement = database_statement_prepare( database, database_insert_investigation_sql ); if (statement == NULL) { return false; } success = database_statement_bind_text( statement, 1, investigation_uuid ) && database_statement_bind_text( statement, 2, investigation_name ) && database_statement_bind_text( statement, 3, investigation_root_path ) && database_statement_bind_text( statement, 4, created_at ) && database_statement_bind_text( statement, 5, created_at ) && database_statement_step(statement) == DATABASE_STATEMENT_STEP_DONE; database_statement_finalize(statement); return success; } /** * @brief Produit une date UTC au format ISO 8601 attendu. * * @return Une nouvelle chaîne à libérer avec g_free(), ou NULL. */ static char *database_create_utc_timestamp(void) { GDateTime *date_time = NULL; char *timestamp = NULL; date_time = g_date_time_new_now_utc(); if (date_time == NULL) { return NULL; } timestamp = g_date_time_format( date_time, "%Y-%m-%dT%H:%M:%SZ" ); g_date_time_unref(date_time); return timestamp; } sqlite3 *database_get_handle( Database *database ) { if (database == NULL) { return NULL; } return database->handle; } bool database_get_transaction_active( const Database *database ) { if (database == NULL) { return false; } return database->transaction_active; } /** * @brief Requête de lecture de la version du schéma. */ static const char *const database_select_schema_version_sql = "SELECT value " "FROM metadata " "WHERE key = ?;"; /** * @brief Requête de mise à jour de la version du schéma. */ static const char *const database_update_schema_version_sql = "UPDATE metadata " "SET value = ? " "WHERE key = ?;"; void database_set_transaction_active( Database *database, bool transaction_active ) { if (database == NULL) { return; } database->transaction_active = transaction_active; } /** * @brief Lit et valide la version enregistrée dans metadata. */ static bool database_read_schema_version( Database *database, int *out_schema_version ) { DatabaseStatement *statement = NULL; DatabaseStatementStepResult step_result; char *version_text = NULL; char *end_pointer = NULL; gint64 parsed_version = 0; bool success = false; if (database == NULL || out_schema_version == NULL) { return false; } *out_schema_version = 0; statement = database_statement_prepare( database, database_select_schema_version_sql ); if (statement == NULL) { return false; } if (!database_statement_bind_text( statement, 1, "schema_version" )) { goto cleanup; } step_result = database_statement_step( statement ); if (step_result == DATABASE_STATEMENT_STEP_ERROR) { goto cleanup; } if (step_result == DATABASE_STATEMENT_STEP_DONE) { database_set_error( database, DATABASE_ERROR_INVALID_STATE, "La version du schéma est absente des métadonnées." ); goto cleanup; } if (!database_statement_column_text( statement, 0, &version_text ) || version_text == NULL || version_text[0] == '\0') { database_set_error( database, DATABASE_ERROR_INVALID_STATE, "La version du schéma est invalide." ); goto cleanup; } parsed_version = g_ascii_strtoll( version_text, &end_pointer, 10 ); if (end_pointer == version_text || end_pointer == NULL || end_pointer[0] != '\0' || parsed_version < 1 || parsed_version > G_MAXINT) { database_set_error( database, DATABASE_ERROR_INVALID_STATE, "La version du schéma n’est pas un entier valide." ); goto cleanup; } /* * La clé metadata est primaire, mais on vérifie malgré tout * que la requête ne retourne aucune seconde ligne. */ step_result = database_statement_step( statement ); if (step_result != DATABASE_STATEMENT_STEP_DONE) { if (step_result == DATABASE_STATEMENT_STEP_ROW) { database_set_error( database, DATABASE_ERROR_INVALID_STATE, "Plusieurs versions du schéma sont enregistrées." ); } goto cleanup; } *out_schema_version = (int) parsed_version; database_clear_error_internal( database ); success = true; cleanup: g_free( version_text ); database_statement_finalize( statement ); return success; } /** * @brief Met à jour la version enregistrée dans metadata. */ static bool database_update_schema_version( Database *database, const char *version_text ) { DatabaseStatement *statement = NULL; bool success = false; if (database == NULL || version_text == NULL || version_text[0] == '\0') { return false; } statement = database_statement_prepare( database, database_update_schema_version_sql ); if (statement == NULL) { return false; } success = database_statement_bind_text( statement, 1, version_text ) && database_statement_bind_text( statement, 2, "schema_version" ) && database_statement_step( statement ) == DATABASE_STATEMENT_STEP_DONE; database_statement_finalize( statement ); return success; } /** * @brief Applique atomiquement la migration du schéma V1 vers V2. */ static bool database_migrate_v1_to_v2( Database *database ) { bool transaction_started = false; if (database == NULL) { return false; } if (!database_transaction_begin( database )) { return false; } transaction_started = true; if (!schema_install_v2( database )) { goto rollback; } if (!database_update_schema_version( database, "2" )) { goto rollback; } if (!database_transaction_commit( database )) { goto rollback; } transaction_started = false; return true; rollback: if (transaction_started) { if (!database_transaction_rollback( database )) { g_warning( "Impossible d’annuler la migration SQLite V1 vers V2." ); } } return false; } /** * @brief Applique atomiquement la migration du schéma V2 vers V3. */ static bool database_migrate_v2_to_v3( Database *database ) { bool transaction_started = false; if (database == NULL || !database_transaction_begin(database)) return false; transaction_started = true; if (!schema_install_v3(database) || !database_update_schema_version(database, "3") || !database_transaction_commit(database)) goto rollback; return true; rollback: if (transaction_started && !database_transaction_rollback(database)) g_warning("Impossible d’annuler la migration SQLite V2 vers V3."); return false; } /** @brief Applique atomiquement la migration du schéma V3 vers V4. */ static bool database_migrate_v3_to_v4(Database *database) { bool transaction_started = false; if (database == NULL || !database_transaction_begin(database)) return false; transaction_started = true; if (!schema_install_v4(database) || !database_update_schema_version(database, "4") || !database_transaction_commit(database)) goto rollback; return true; rollback: if (transaction_started && !database_transaction_rollback(database)) g_warning("Impossible d’annuler la migration SQLite V3 vers V4."); return false; } /** @brief Applique atomiquement la migration du schéma V4 vers V5. */ static bool database_migrate_v4_to_v5(Database *database) { bool transaction_started = false; if (database == NULL || !database_transaction_begin(database)) return false; transaction_started = true; if (!schema_install_v5(database) || !database_update_schema_version(database, "5") || !database_transaction_commit(database)) goto rollback; return true; rollback: if (transaction_started && !database_transaction_rollback(database)) g_warning("Impossible d’annuler la migration SQLite V4 vers V5."); return false; } /** @brief Applique atomiquement la migration du schéma V5 vers V6. */ static bool database_migrate_v5_to_v6(Database *database) { bool transaction_started = false; if (database == NULL || !database_transaction_begin(database)) return false; transaction_started = true; if (!schema_install_v6(database) || !database_update_schema_version(database, "6") || !database_transaction_commit(database)) goto rollback; return true; rollback: if (transaction_started && !database_transaction_rollback(database)) g_warning("Impossible d’annuler la migration SQLite V5 vers V6."); return false; } static bool database_migrate_v6_to_v7(Database *database) { bool transaction_started = false; if (database == NULL || !database_transaction_begin(database)) return false; transaction_started = true; if (!schema_install_v7(database) || !database_update_schema_version(database, "7") || !database_transaction_commit(database)) goto rollback; return true; rollback: if (transaction_started && !database_transaction_rollback(database)) g_warning("Impossible d’annuler la migration SQLite V6 vers V7."); return false; } static bool database_migrate_v7_to_v8(Database *database) { bool transaction_started = false; if (database == NULL || !database_transaction_begin(database)) return false; transaction_started = true; if (!schema_install_v8(database) || !database_update_schema_version(database, "8") || !database_transaction_commit(database)) goto rollback; return true; rollback: if (transaction_started && !database_transaction_rollback(database)) g_warning("Impossible d’annuler la migration SQLite V7 vers V8."); return false; } /** * @brief Garantit atomiquement la présence des extensions du schéma courant. */ static bool database_ensure_current_schema( Database *database ) { bool transaction_started = false; if (database == NULL) { return false; } if (!database_transaction_begin( database )) { return false; } transaction_started = true; if (!schema_ensure_current( database )) { goto rollback; } if (!database_transaction_commit( database )) { goto rollback; } transaction_started = false; return true; rollback: if (transaction_started) { if (!database_transaction_rollback( database )) { g_warning( "Impossible d’annuler l’installation des extensions " "du schéma SQLite courant." ); } } return false; } Database *database_open( const char *database_path ) { Database *database = NULL; int result = SQLITE_ERROR; if (database_path == NULL || database_path[0] == '\0') { return NULL; } database = g_new0(Database, 1); if (database == NULL) { return NULL; } database->database_path = g_strdup(database_path); if (database->database_path == NULL) { g_free(database); return NULL; } result = sqlite3_open_v2( database_path, &database->handle, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_PRIVATECACHE, NULL ); if (result != SQLITE_OK) { g_warning( "Impossible d'ouvrir la base '%s' : %s", database_path, database->handle != NULL ? sqlite3_errmsg(database->handle) : sqlite3_errstr(result) ); database_close(database); return NULL; } if (!database_execute_sql( database->handle, "PRAGMA foreign_keys = ON;" )) { database_close(database); return NULL; } database->transaction_active = false; database->schema_version = 0; return database; } bool database_migrate_to_latest( Database *database ) { int schema_version = 0; if (database == NULL) { return false; } if (database_get_transaction_active( database )) { database_set_error( database, DATABASE_ERROR_INVALID_STATE, "La migration est impossible pendant une transaction active." ); return false; } if (!database_read_schema_version( database, &schema_version )) { return false; } if (schema_version > DATABASE_SCHEMA_VERSION_CURRENT) { database_set_error( database, DATABASE_ERROR_INVALID_STATE, "La base utilise une version de schéma plus récente " "que cette version de Labfy Investigation." ); return false; } while (schema_version < DATABASE_SCHEMA_VERSION_CURRENT) { switch (schema_version) { case 1: if (!database_migrate_v1_to_v2( database )) { return false; } schema_version = 2; break; case 2: if (!database_migrate_v2_to_v3(database)) return false; schema_version = 3; break; case 3: if (!database_migrate_v3_to_v4(database)) return false; schema_version = 4; break; case 4: if (!database_migrate_v4_to_v5(database)) return false; schema_version = 5; break; case 5: if (!database_migrate_v5_to_v6(database)) return false; schema_version = 6; break; case 6: if (!database_migrate_v6_to_v7(database)) return false; schema_version = 7; break; case 7: if (!database_migrate_v7_to_v8(database)) return false; schema_version = 8; break; default: database_set_error( database, DATABASE_ERROR_INVALID_STATE, "La version du schéma ne possède aucune " "migration prise en charge." ); return false; } } if (!database_ensure_current_schema( database )) { return false; } database->schema_version = schema_version; database_clear_error_internal( database ); return true; } bool database_initialize( const char *database_path, const char *investigation_name, const char *investigation_root_path ) { Database *database = NULL; char *created_at = NULL; char *investigation_uuid = NULL; bool transaction_started = false; bool success = false; if (!database_validate_initialize_parameters( database_path, investigation_name, investigation_root_path )) { return false; } created_at = database_create_utc_timestamp(); if (created_at == NULL) { return false; } investigation_uuid = g_uuid_string_random(); if (investigation_uuid == NULL) { goto cleanup; } database = database_open( database_path ); if (database == NULL) { goto cleanup; } /* * Accès temporaire au handle SQLite. * * Il reste nécessaire tant que schema_install_v1() et les anciennes * fonctions d'insertion n'ont pas encore été migrées vers Database. */ if (!database_transaction_begin( database )) { goto cleanup; } transaction_started = true; if (!schema_install_v1( database )) { goto rollback; } /* * Une nouvelle base reçoit immédiatement toutes les versions du schéma * dans la transaction initiale. */ if (!schema_install_v2( database )) { goto rollback; } if (!schema_install_v3( database )) { goto rollback; } if (!schema_install_v4(database)) { goto rollback; } if (!schema_install_v5(database)) { goto rollback; } if (!schema_install_v6(database)) { goto rollback; } if (!schema_ensure_current( database )) { goto rollback; } if (!database_insert_all_metadata( database, created_at, investigation_uuid )) { goto rollback; } if (!database_insert_investigation( database, investigation_uuid, investigation_name, investigation_root_path, created_at )) { goto rollback; } if (!database_transaction_commit( database )) { goto rollback; } transaction_started = false; success = true; goto cleanup; rollback: if (transaction_started) { if (!database_transaction_rollback( database )) { g_warning( "Échec du rollback de la base '%s'.", database_path ); } transaction_started = false; } cleanup: database_close(database); g_free(investigation_uuid); g_free(created_at); return success; } void database_set_error( Database *database, DatabaseErrorCode error_code, const char *error_message ) { char *error_message_copy = NULL; if (database == NULL) { return; } if (error_message != NULL) { error_message_copy = g_strdup(error_message); } g_free(database->error_message); database->error_code = error_code; database->error_message = error_message_copy; } DatabaseErrorCode database_get_error_code_internal( const Database *database ) { if (database == NULL) { return DATABASE_ERROR_NONE; } return database->error_code; } const char *database_get_error_message_internal( const Database *database ) { if (database == NULL) { return NULL; } return database->error_message; } void database_clear_error_internal( Database *database ) { if (database == NULL) { return; } g_free(database->error_message); database->error_message = NULL; database->error_code = DATABASE_ERROR_NONE; } void database_close( Database *database ) { int result = SQLITE_OK; if (database == NULL) { return; } if (database->handle != NULL) { result = sqlite3_close(database->handle); if (result != SQLITE_OK) { g_warning( "Impossible de fermer proprement la base '%s' : %s", database->database_path != NULL ? database->database_path : "(chemin inconnu)", sqlite3_errstr(result) ); } } g_free(database->error_message); g_free(database->database_path); g_free(database); }