feat: persist evidence records

This commit is contained in:
grayTerminal-sh 2026-07-18 19:18:51 +02:00
parent 7d6f7f672f
commit c7bf0da4e6
24 changed files with 4940 additions and 53 deletions

View file

@ -43,6 +43,7 @@ TEST_INVESTIGATION_RECORD = tests/test_investigation_record
TEST_EVIDENCE_RECORD := tests/test_evidence_record TEST_EVIDENCE_RECORD := tests/test_evidence_record
TEST_EVIDENCE_RECORD := tests/test_evidence_record TEST_EVIDENCE_RECORD := tests/test_evidence_record
TEST_INVESTIGATION_DAO := tests/test_investigation_dao TEST_INVESTIGATION_DAO := tests/test_investigation_dao
TEST_EVIDENCE_DAO := tests/test_evidence_dao
TEST_INVESTIGATION_SESSION := tests/test_investigation_session TEST_INVESTIGATION_SESSION := tests/test_investigation_session
TEST_BACKGROUND_TASK := tests/test_background_task TEST_BACKGROUND_TASK := tests/test_background_task
TEST_TASK_MANAGER := tests/test_task_manager TEST_TASK_MANAGER := tests/test_task_manager
@ -90,7 +91,8 @@ $(TEST_DATABASE): \
src/database/database.c \ src/database/database.c \
src/database/transaction.c \ src/database/transaction.c \
src/database/statement.c \ src/database/statement.c \
src/database/schema.c src/database/schema.c \
src/database/error.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3
$(TEST_STATEMENT): \ $(TEST_STATEMENT): \
@ -140,6 +142,17 @@ $(TEST_INVESTIGATION_DAO): \
src/database/error.c src/database/error.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3
$(TEST_EVIDENCE_DAO): \
tests/test_evidence_dao.c \
src/dao/evidence_dao.c \
src/models/evidence_record.c \
src/database/database.c \
src/database/schema.c \
src/database/statement.c \
src/database/transaction.c \
src/database/error.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3
$(TEST_INVESTIGATION_SESSION): \ $(TEST_INVESTIGATION_SESSION): \
tests/test_investigation_session.c \ tests/test_investigation_session.c \
src/core/investigation_session.c \ src/core/investigation_session.c \
@ -211,6 +224,7 @@ test: \
$(TEST_INVESTIGATION_RECORD) \ $(TEST_INVESTIGATION_RECORD) \
$(TEST_EVIDENCE_RECORD) \ $(TEST_EVIDENCE_RECORD) \
$(TEST_INVESTIGATION_DAO) \ $(TEST_INVESTIGATION_DAO) \
$(TEST_EVIDENCE_DAO) \
$(TEST_INVESTIGATION_SESSION) \ $(TEST_INVESTIGATION_SESSION) \
$(TEST_BACKGROUND_TASK) \ $(TEST_BACKGROUND_TASK) \
$(TEST_TASK_MANAGER) \ $(TEST_TASK_MANAGER) \
@ -231,6 +245,7 @@ test: \
@$(TEST_INVESTIGATION_RECORD) @$(TEST_INVESTIGATION_RECORD)
@$(TEST_EVIDENCE_RECORD) @$(TEST_EVIDENCE_RECORD)
@$(TEST_INVESTIGATION_DAO) @$(TEST_INVESTIGATION_DAO)
@$(TEST_EVIDENCE_DAO)
@$(TEST_INVESTIGATION_SESSION) @$(TEST_INVESTIGATION_SESSION)
@$(TEST_BACKGROUND_TASK) @$(TEST_BACKGROUND_TASK)
@$(TEST_TASK_MANAGER) @$(TEST_TASK_MANAGER)
@ -260,6 +275,7 @@ clean:
$(TEST_INVESTIGATION_RECORD) \ $(TEST_INVESTIGATION_RECORD) \
$(TEST_EVIDENCE_RECORD) \ $(TEST_EVIDENCE_RECORD) \
$(TEST_INVESTIGATION_DAO) \ $(TEST_INVESTIGATION_DAO) \
$(TEST_EVIDENCE_DAO) \
$(TEST_INVESTIGATION_SESSION) \ $(TEST_INVESTIGATION_SESSION) \
$(TEST_BACKGROUND_TASK) \ $(TEST_BACKGROUND_TASK) \
$(TEST_TASK_MANAGER) \ $(TEST_TASK_MANAGER) \

105
database/schema_v2.sql Normal file
View file

@ -0,0 +1,105 @@
/******************************************************************************
* Labfy Investigation
*
* Migration du schéma SQLite V1 vers V2
******************************************************************************/
/*
* Nom du fichier tel quil existait avant son import.
*
* La colonne name existante conserve le nom interne utilisé dans
* larborescence de lenquête.
*/
ALTER TABLE preuves
ADD COLUMN original_name TEXT;
/*
* Date déclarée de collecte de la preuve.
*
* Elle est distincte de :
*
* - file_created_at : date technique connue du fichier ;
* - imported_at : date dimport dans lenquête.
*/
ALTER TABLE preuves
ADD COLUMN collected_at TEXT;
/*
* Description textuelle initiale de la provenance.
*
* Une future évolution pourra relier une preuve à la table sources
* avec un identifiant métier.
*/
ALTER TABLE preuves
ADD COLUMN source TEXT;
/*
* Correspondance avec EvidenceIntegrityStatus :
*
* 0 = UNKNOWN
* 1 = VALID
* 2 = MISSING
* 3 = MODIFIED
* 4 = ERROR
*/
ALTER TABLE preuves
ADD COLUMN integrity_status INTEGER NOT NULL DEFAULT 0
CHECK (
integrity_status BETWEEN 0 AND 4
);
/*
* Les éventuelles preuves V1 utilisent leur ancien nom visible comme
* nom original afin de rester lisibles après migration.
*/
UPDATE preuves
SET original_name = name
WHERE original_name IS NULL
OR length(trim(original_name)) = 0;
CREATE INDEX idx_preuves_imported_at
ON preuves(imported_at);
/*
* SQLite ne permet pas dajouter directement une contrainte NOT NULL
* à une colonne ajoutée lorsque des lignes peuvent déjà exister.
*
* Ces triggers renforcent donc les insertions et modifications V2.
*/
CREATE TRIGGER preuves_v2_validate_insert
BEFORE INSERT ON preuves
FOR EACH ROW
WHEN
NEW.original_name IS NULL
OR length(trim(NEW.original_name)) = 0
OR NEW.size_bytes IS NULL
OR NEW.size_bytes < 0
OR NEW.sha256 IS NULL
OR length(NEW.sha256) != 64
OR NEW.sha256 != lower(NEW.sha256)
OR NEW.integrity_status NOT BETWEEN 0 AND 4
BEGIN
SELECT RAISE(
ABORT,
'La preuve ne respecte pas les contraintes du schéma V2.'
);
END;
CREATE TRIGGER preuves_v2_validate_update
BEFORE UPDATE ON preuves
FOR EACH ROW
WHEN
NEW.original_name IS NULL
OR length(trim(NEW.original_name)) = 0
OR NEW.size_bytes IS NULL
OR NEW.size_bytes < 0
OR NEW.sha256 IS NULL
OR length(NEW.sha256) != 64
OR NEW.sha256 != lower(NEW.sha256)
OR NEW.integrity_status NOT BETWEEN 0 AND 4
BEGIN
SELECT RAISE(
ABORT,
'La preuve ne respecte pas les contraintes du schéma V2.'
);
END;

142
include/dao/evidence_dao.h Normal file
View file

@ -0,0 +1,142 @@
/******************************************************************************
* @file evidence_dao.h
* @brief Persistance des preuves numériques dans SQLite.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_EVIDENCE_DAO_H
#define LABFY_INVESTIGATION_EVIDENCE_DAO_H
#include "database/database.h"
#include "models/evidence_record.h"
#include <glib.h>
/**
* @brief Catégories derreurs du DAO des preuves.
*/
typedef enum
{
EVIDENCE_DAO_ERROR_INVALID_ARGUMENT,
EVIDENCE_DAO_ERROR_MEMORY,
EVIDENCE_DAO_ERROR_PREPARE,
EVIDENCE_DAO_ERROR_BIND,
EVIDENCE_DAO_ERROR_EXECUTE,
EVIDENCE_DAO_ERROR_CONSTRAINT,
EVIDENCE_DAO_ERROR_READ,
EVIDENCE_DAO_ERROR_MODEL,
EVIDENCE_DAO_ERROR_SCHEMA,
EVIDENCE_DAO_ERROR_RANGE
} EvidenceDaoError;
/**
* @brief Domaine derreurs du DAO des preuves.
*/
#define EVIDENCE_DAO_ERROR \
evidence_dao_error_quark()
/**
* @brief Retourne le domaine derreurs du DAO des preuves.
*/
GQuark evidence_dao_error_quark(void);
/**
* @brief DAO opaque donnant accès aux preuves persistées.
*
* Lobjet emprunte la connexion Database reçue lors de sa création.
*/
typedef struct EvidenceDao EvidenceDao;
/**
* @brief Crée un DAO utilisant une connexion Database existante.
*
* La connexion est empruntée et doit rester valide pendant toute la durée
* de vie du DAO.
*
* @param database Connexion ouverte.
* @param error Adresse recevant une éventuelle erreur.
*
* @return Nouveau DAO, ou NULL en cas déchec.
*/
EvidenceDao *evidence_dao_new(
Database *database,
GError **error
);
/**
* @brief Libère le DAO.
*
* La connexion Database empruntée nest pas fermée.
* Cette fonction accepte NULL.
*
* @param evidence_dao DAO à libérer.
*/
void evidence_dao_free(
EvidenceDao *evidence_dao
);
/**
* @brief Insère une nouvelle preuve.
*
* Aucun enregistrement existant nest remplacé ou modifié.
*
* @param evidence_dao DAO valide.
* @param evidence_record Preuve empruntée.
* @param error Adresse recevant une éventuelle erreur.
*
* @return TRUE si linsertion réussit, sinon FALSE.
*/
gboolean evidence_dao_insert(
EvidenceDao *evidence_dao,
const EvidenceRecord *evidence_record,
GError **error
);
/**
* @brief Recherche une preuve par son identifiant.
*
* Une preuve absente retourne NULL sans produire derreur.
*
* @param evidence_dao DAO valide.
* @param identifier Identifiant UUID recherché.
* @param error Adresse recevant une éventuelle erreur.
*
* @return Nouveau modèle possédé par lappelant, ou NULL.
*/
EvidenceRecord *evidence_dao_find_by_identifier(
EvidenceDao *evidence_dao,
const char *identifier,
GError **error
);
/**
* @brief Charge toutes les preuves dans un ordre déterministe.
*
* Le tableau retourné utilise evidence_record_free() comme fonction
* de destruction.
*
* @param evidence_dao DAO valide.
* @param error Adresse recevant une éventuelle erreur.
*
* @return Nouveau GPtrArray, ou NULL en cas déchec.
*/
GPtrArray *evidence_dao_list_all(
EvidenceDao *evidence_dao,
GError **error
);
/**
* @brief Compte les preuves persistées.
*
* @param evidence_dao DAO valide.
* @param out_count Destination du nombre de preuves.
* @param error Adresse recevant une éventuelle erreur.
*
* @return TRUE si le comptage réussit, sinon FALSE.
*/
gboolean evidence_dao_count(
EvidenceDao *evidence_dao,
guint64 *out_count,
GError **error
);
#endif

View file

@ -42,6 +42,26 @@ void database_close(
Database *database Database *database
); );
/**
* @brief Met à jour une base ouverte vers la dernière version du schéma.
*
* La fonction :
*
* - lit metadata.schema_version ;
* - applique chaque migration manquante dans une transaction ;
* - met à jour la version uniquement après une migration réussie ;
* - ne modifie rien lorsque la base est déjà à jour.
*
* La fonction refuse une migration lorsquune transaction est déjà active.
*
* @param database Connexion Database ouverte.
*
* @return true si la base est à jour, sinon false.
*/
bool database_migrate_to_latest(
Database *database
);
/** /**
* @brief Initialise la base SQLite d'une nouvelle enquête. * @brief Initialise la base SQLite d'une nouvelle enquête.
* *

View file

@ -31,4 +31,27 @@ bool schema_install_v1(
Database *database Database *database
); );
/**
* @brief Installe les modifications du schéma SQLite V2.
*
* La connexion doit être valide et une transaction doit déjà être active.
*
* Cette fonction :
*
* - étend la table preuves ;
* - conserve les éventuelles lignes V1 ;
* - ajoute les contraintes nécessaires aux nouvelles preuves ;
* - ajoute les index V2.
*
* Elle ne modifie pas elle-même la version enregistrée dans metadata.
* Elle ne réalise ni COMMIT ni ROLLBACK.
*
* @param database Connexion Database ouverte.
*
* @return true si la migration V2 a é appliquée, sinon false.
*/
bool schema_install_v2(
Database *database
);
#endif #endif

View file

@ -186,23 +186,6 @@ bool database_statement_bind_null(
int index int index
); );
/**
* @brief Lie une chaîne de caractères à un paramètre SQL.
*
* Les indices des paramètres SQLite commencent à 1.
*
* @param statement Requête préparée.
* @param index Indice du paramètre SQL.
* @param value Chaîne de caractères à lier.
*
* @return true en cas de succès, sinon false.
*/
bool database_statement_bind_text(
DatabaseStatement *statement,
int index,
const char *value
);
/** /**
* @brief Finalise une requête préparée et libère ses ressources. * @brief Finalise une requête préparée et libère ses ressources.
* *

Binary file not shown.

View file

@ -192,6 +192,33 @@ InvestigationSession *investigation_session_open(
goto cleanup; goto cleanup;
} }
if (!database_migrate_to_latest(
database
))
{
database_error_message =
database_error_get_message(
database
);
if (database_error_message == NULL ||
database_error_message[0] == '\0')
{
database_error_message =
"La base de données na pas pu être mise à jour.";
}
g_set_error(
error,
INVESTIGATION_SESSION_ERROR,
INVESTIGATION_SESSION_ERROR_DATABASE,
"Impossible de mettre à jour le schéma SQLite : %s",
database_error_message
);
goto cleanup;
}
record = investigation_dao_load( record = investigation_dao_load(
database database
); );

1482
src/dao/evidence_dao.c Normal file

File diff suppressed because it is too large Load diff

View file

@ -16,7 +16,12 @@
/** /**
* @brief Version actuelle du schéma SQLite. * @brief Version actuelle du schéma SQLite.
*/ */
#define DATABASE_SCHEMA_VERSION "1" #define DATABASE_SCHEMA_VERSION_CURRENT 2
/**
* @brief Version actuelle sous forme textuelle pour metadata.
*/
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "2"
/** /**
* @brief Nom de l'application enregistré dans les métadonnées. * @brief Nom de l'application enregistré dans les métadonnées.
@ -225,7 +230,7 @@ static bool database_insert_all_metadata(
database_insert_metadata( database_insert_metadata(
statement, statement,
"schema_version", "schema_version",
DATABASE_SCHEMA_VERSION DATABASE_SCHEMA_VERSION_CURRENT_TEXT
) && ) &&
database_insert_metadata( database_insert_metadata(
statement, statement,
@ -366,6 +371,22 @@ bool database_get_transaction_active(
return database->transaction_active; 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( void database_set_transaction_active(
Database *database, Database *database,
bool transaction_active bool transaction_active
@ -379,6 +400,274 @@ void database_set_transaction_active(
database->transaction_active = transaction_active; 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 nest 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 dannuler la migration SQLite V1 vers V2."
);
}
}
return false;
}
Database *database_open( Database *database_open(
const char *database_path const char *database_path
) )
@ -445,6 +734,89 @@ Database *database_open(
return database; 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;
default:
database_set_error(
database,
DATABASE_ERROR_INVALID_STATE,
"La version du schéma ne possède aucune "
"migration prise en charge."
);
return false;
}
}
database->schema_version =
schema_version;
database_clear_error_internal(
database
);
return true;
}
bool database_initialize( bool database_initialize(
const char *database_path, const char *database_path,
const char *investigation_name, const char *investigation_name,
@ -514,6 +886,17 @@ bool database_initialize(
goto rollback; 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 (!database_insert_all_metadata( if (!database_insert_all_metadata(
database, database,
created_at, created_at,

View file

@ -1,6 +1,6 @@
/****************************************************************************** /******************************************************************************
* @file schema.c * @file schema.c
* @brief Installation du schéma SQLite de Labfy Investigation. * @brief Installation des versions du schéma SQLite.
******************************************************************************/ ******************************************************************************/
#include "database/schema.h" #include "database/schema.h"
@ -11,22 +11,34 @@
#include <sqlite3.h> #include <sqlite3.h>
/** /**
* @brief Charge le schéma SQL V1 depuis le fichier du projet. * @brief Charge un fichier SQL du projet.
* *
* @param database Connexion utilisée pour enregistrer une éventuelle erreur. * @param database Connexion recevant les erreurs.
* @param schema_path Chemin du fichier SQL.
* @param schema_name Nom utilisé dans les diagnostics.
* *
* @return Une nouvelle chaîne terminée par zéro, à libérer avec g_free(), * @return Nouvelle chaîne SQL, ou NULL.
* ou NULL en cas d'échec.
*/ */
static char *schema_load_v1_sql( static char *schema_load_sql(
Database *database Database *database,
const char *schema_path,
const char *schema_name
) )
{ {
char *schema_sql = NULL; char *schema_sql = NULL;
GError *error = NULL; GError *error = NULL;
if (database == NULL ||
schema_path == NULL ||
schema_path[0] == '\0' ||
schema_name == NULL ||
schema_name[0] == '\0')
{
return NULL;
}
if (!g_file_get_contents( if (!g_file_get_contents(
"database/schema_v1.sql", schema_path,
&schema_sql, &schema_sql,
NULL, NULL,
&error &error
@ -37,17 +49,21 @@ static char *schema_load_v1_sql(
DATABASE_ERROR_INVALID_STATE, DATABASE_ERROR_INVALID_STATE,
error != NULL error != NULL
? error->message ? error->message
: "Impossible de charger le schéma SQLite V1." : "Impossible de charger le schéma SQLite."
); );
g_warning( g_warning(
"Impossible de charger database/schema_v1.sql : %s", "Impossible de charger %s depuis '%s' : %s",
schema_name,
schema_path,
error != NULL error != NULL
? error->message ? error->message
: "erreur inconnue" : "erreur inconnue"
); );
g_clear_error(&error); g_clear_error(
&error
);
return NULL; return NULL;
} }
@ -55,11 +71,19 @@ static char *schema_load_v1_sql(
return schema_sql; return schema_sql;
} }
bool schema_install_v1( /**
Database *database * @brief Exécute le contenu dun fichier de schéma SQL.
*
* La transaction reste sous la responsabilité du code appelant.
*/
static bool schema_execute_file(
Database *database,
const char *schema_path,
const char *schema_name
) )
{ {
sqlite3 *database_handle = NULL; sqlite3 *database_handle = NULL;
char *schema_sql = NULL; char *schema_sql = NULL;
char *error_message = NULL; char *error_message = NULL;
@ -70,7 +94,8 @@ bool schema_install_v1(
return false; return false;
} }
database_handle = database_get_handle( database_handle =
database_get_handle(
database database
); );
@ -85,8 +110,11 @@ bool schema_install_v1(
return false; return false;
} }
schema_sql = schema_load_v1_sql( schema_sql =
database schema_load_sql(
database,
schema_path,
schema_name
); );
if (schema_sql == NULL) if (schema_sql == NULL)
@ -94,7 +122,8 @@ bool schema_install_v1(
return false; return false;
} }
result = sqlite3_exec( result =
sqlite3_exec(
database_handle, database_handle,
schema_sql, schema_sql,
NULL, NULL,
@ -102,7 +131,9 @@ bool schema_install_v1(
&error_message &error_message
); );
g_free(schema_sql); g_free(
schema_sql
);
if (result != SQLITE_OK) if (result != SQLITE_OK)
{ {
@ -115,18 +146,23 @@ bool schema_install_v1(
); );
g_warning( g_warning(
"Impossible d'installer le schéma SQLite V1 : %s", "Impossible dinstaller %s : %s",
schema_name,
error_message != NULL error_message != NULL
? error_message ? error_message
: sqlite3_errmsg(database_handle) : sqlite3_errmsg(database_handle)
); );
sqlite3_free(error_message); sqlite3_free(
error_message
);
return false; return false;
} }
sqlite3_free(error_message); sqlite3_free(
error_message
);
database_clear_error_internal( database_clear_error_internal(
database database
@ -134,3 +170,25 @@ bool schema_install_v1(
return true; return true;
} }
bool schema_install_v1(
Database *database
)
{
return schema_execute_file(
database,
"database/schema_v1.sql",
"le schéma SQLite V1"
);
}
bool schema_install_v2(
Database *database
)
{
return schema_execute_file(
database,
"database/schema_v2.sql",
"la migration SQLite V2"
);
}

Binary file not shown.

View file

@ -4,6 +4,7 @@
******************************************************************************/ ******************************************************************************/
#include "database/database.h" #include "database/database.h"
#include "database/error.h"
#include <assert.h> #include <assert.h>
#include <stdio.h> #include <stdio.h>
@ -69,6 +70,183 @@ static char *test_database_read_single_text(
return result_text; return result_text;
} }
/**
* @brief Exécute une ou plusieurs instructions SQL de test.
*/
static void test_database_execute_sql(
sqlite3 *database,
const char *sql
)
{
char *error_message = NULL;
int result = SQLITE_ERROR;
assert(database != NULL);
assert(sql != NULL);
assert(sql[0] != '\0');
result = sqlite3_exec(
database,
sql,
NULL,
NULL,
&error_message
);
if (result != SQLITE_OK)
{
fprintf(
stderr,
"Erreur SQL de test : %s\n",
error_message != NULL
? error_message
: sqlite3_errmsg(database)
);
}
assert(result == SQLITE_OK);
sqlite3_free(
error_message
);
}
/**
* @brief Crée une base V1 contenant une ancienne preuve.
*
* La base est construite directement avec schema_v1.sql afin de tester
* le véritable chemin de migration, sans passer par database_initialize()
* qui crée désormais directement une base V2.
*/
static void test_database_create_v1_database(
const char *database_path
)
{
sqlite3 *database = NULL;
char *schema_sql = NULL;
GError *error = NULL;
int result = SQLITE_ERROR;
assert(database_path != NULL);
assert(database_path[0] != '\0');
result = sqlite3_open_v2(
database_path,
&database,
SQLITE_OPEN_READWRITE |
SQLITE_OPEN_CREATE,
NULL
);
assert(result == SQLITE_OK);
assert(database != NULL);
assert(
g_file_get_contents(
"database/schema_v1.sql",
&schema_sql,
NULL,
&error
)
);
assert(error == NULL);
assert(schema_sql != NULL);
test_database_execute_sql(
database,
"BEGIN IMMEDIATE;"
);
test_database_execute_sql(
database,
schema_sql
);
test_database_execute_sql(
database,
"INSERT INTO metadata"
"("
" key,"
" value"
")"
"VALUES"
" ('schema_version', '1'),"
" ('application', 'Labfy Investigation'),"
" ('created_at', '2026-07-18T10:00:00Z'),"
" ("
" 'investigation_uuid',"
" '11111111-1111-4111-8111-111111111111'"
" );"
);
/*
* Cette ligne utilise uniquement les colonnes du schéma V1.
*
* La migration devra conserver la ligne et copier name dans
* original_name.
*/
test_database_execute_sql(
database,
"INSERT INTO preuves"
"("
" id,"
" name,"
" relative_path,"
" type_id,"
" size_bytes,"
" sha256,"
" mime_type,"
" description,"
" commentaire,"
" categorie_id,"
" file_created_at,"
" imported_at,"
" updated_at,"
" status,"
" locked"
")"
"VALUES"
"("
" '22222222-2222-4222-8222-222222222222',"
" 'ancienne_capture.png',"
" '01_Preuves_Originales/ancienne_capture.png',"
" (SELECT id FROM types_preuve "
" WHERE code = 'screenshot'),"
" 128,"
" '0123456789abcdef0123456789abcdef"
"0123456789abcdef0123456789abcdef',"
" 'image/png',"
" 'Preuve créée avec le schéma V1.',"
" NULL,"
" NULL,"
" NULL,"
" '2026-07-18T10:00:00Z',"
" '2026-07-18T10:00:00Z',"
" 'active',"
" 1"
");"
);
test_database_execute_sql(
database,
"COMMIT;"
);
g_free(
schema_sql
);
result = sqlite3_close(
database
);
assert(result == SQLITE_OK);
}
/** /**
* @brief Vérifie qu'une table existe. * @brief Vérifie qu'une table existe.
*/ */
@ -116,6 +294,75 @@ static void test_database_assert_table_exists(
assert(result == SQLITE_OK); assert(result == SQLITE_OK);
} }
/**
* @brief Vérifie quune colonne existe dans une table.
*/
static void test_database_assert_column_exists(
sqlite3 *database,
const char *table_name,
const char *column_name
)
{
sqlite3_stmt *statement = NULL;
int result = SQLITE_ERROR;
assert(database != NULL);
assert(table_name != NULL);
assert(column_name != NULL);
result = sqlite3_prepare_v2(
database,
"SELECT COUNT(*) "
"FROM pragma_table_info(?) "
"WHERE name = ?;",
-1,
&statement,
NULL
);
assert(result == SQLITE_OK);
assert(statement != NULL);
result = sqlite3_bind_text(
statement,
1,
table_name,
-1,
SQLITE_TRANSIENT
);
assert(result == SQLITE_OK);
result = sqlite3_bind_text(
statement,
2,
column_name,
-1,
SQLITE_TRANSIENT
);
assert(result == SQLITE_OK);
result = sqlite3_step(
statement
);
assert(result == SQLITE_ROW);
assert(
sqlite3_column_int(
statement,
0
) == 1
);
result = sqlite3_finalize(
statement
);
assert(result == SQLITE_OK);
}
/** /**
* @brief Vérifie l'initialisation complète d'une base. * @brief Vérifie l'initialisation complète d'une base.
*/ */
@ -189,6 +436,34 @@ static void test_database_initialize_valid_database(void)
database, database,
"investigation" "investigation"
); );
test_database_assert_table_exists(
database,
"preuves"
);
test_database_assert_column_exists(
database,
"preuves",
"original_name"
);
test_database_assert_column_exists(
database,
"preuves",
"collected_at"
);
test_database_assert_column_exists(
database,
"preuves",
"source"
);
test_database_assert_column_exists(
database,
"preuves",
"integrity_status"
);
schema_version = test_database_read_single_text( schema_version = test_database_read_single_text(
database, database,
@ -250,7 +525,7 @@ static void test_database_initialize_valid_database(void)
"FROM investigation;" "FROM investigation;"
); );
assert(strcmp(schema_version, "1") == 0); assert(strcmp(schema_version, "2") == 0);
assert(strcmp(application_name, "Labfy Investigation") == 0); assert(strcmp(application_name, "Labfy Investigation") == 0);
assert(created_at[0] != '\0'); assert(created_at[0] != '\0');
@ -519,10 +794,611 @@ static void test_database_initialize_rollback(void)
g_free(temporary_directory); g_free(temporary_directory);
} }
/**
* @brief Vérifie la migration V1 vers V2 et sa répétition sans effet.
*/
static void test_database_migrate_v1_to_v2(void)
{
char *temporary_directory = NULL;
char *database_path = NULL;
char *schema_version = NULL;
char *original_name = NULL;
char *internal_name = NULL;
char *relative_path = NULL;
char *integrity_status = NULL;
char *evidence_count = NULL;
char *v2_column_count = NULL;
char *migration_index_count = NULL;
char *migration_trigger_count = NULL;
Database *database_context = NULL;
sqlite3 *database = NULL;
GError *error = NULL;
int result = SQLITE_ERROR;
temporary_directory =
g_dir_make_tmp(
"labfy-database-migration-test-XXXXXX",
&error
);
assert(temporary_directory != NULL);
assert(error == NULL);
database_path =
g_build_filename(
temporary_directory,
"Enquete.sqlite",
NULL
);
assert(database_path != NULL);
test_database_create_v1_database(
database_path
);
database_context =
database_open(
database_path
);
assert(database_context != NULL);
/*
* Premier appel : migration réelle de V1 vers V2.
*/
assert(
database_migrate_to_latest(
database_context
)
);
assert(
database_error_get_code(
database_context
) == DATABASE_ERROR_NONE
);
/*
* Second appel : la base est déjà en V2.
*
* Aucun ALTER TABLE, index ou trigger ne doit être rejoué.
*/
assert(
database_migrate_to_latest(
database_context
)
);
assert(
database_error_get_code(
database_context
) == DATABASE_ERROR_NONE
);
database_close(
database_context
);
result = sqlite3_open_v2(
database_path,
&database,
SQLITE_OPEN_READONLY,
NULL
);
assert(result == SQLITE_OK);
assert(database != NULL);
schema_version =
test_database_read_single_text(
database,
"SELECT value "
"FROM metadata "
"WHERE key = 'schema_version';"
);
evidence_count =
test_database_read_single_text(
database,
"SELECT CAST(COUNT(*) AS TEXT) "
"FROM preuves;"
);
internal_name =
test_database_read_single_text(
database,
"SELECT name "
"FROM preuves "
"WHERE id = "
"'22222222-2222-4222-8222-222222222222';"
);
original_name =
test_database_read_single_text(
database,
"SELECT original_name "
"FROM preuves "
"WHERE id = "
"'22222222-2222-4222-8222-222222222222';"
);
relative_path =
test_database_read_single_text(
database,
"SELECT relative_path "
"FROM preuves "
"WHERE id = "
"'22222222-2222-4222-8222-222222222222';"
);
integrity_status =
test_database_read_single_text(
database,
"SELECT CAST(integrity_status AS TEXT) "
"FROM preuves "
"WHERE id = "
"'22222222-2222-4222-8222-222222222222';"
);
v2_column_count =
test_database_read_single_text(
database,
"SELECT CAST(COUNT(*) AS TEXT) "
"FROM pragma_table_info('preuves') "
"WHERE name IN"
"("
" 'original_name',"
" 'collected_at',"
" 'source',"
" 'integrity_status'"
");"
);
migration_index_count =
test_database_read_single_text(
database,
"SELECT CAST(COUNT(*) AS TEXT) "
"FROM sqlite_master "
"WHERE type = 'index' "
"AND name = 'idx_preuves_imported_at';"
);
migration_trigger_count =
test_database_read_single_text(
database,
"SELECT CAST(COUNT(*) AS TEXT) "
"FROM sqlite_master "
"WHERE type = 'trigger' "
"AND name IN"
"("
" 'preuves_v2_validate_insert',"
" 'preuves_v2_validate_update'"
");"
);
assert(
strcmp(
schema_version,
"2"
) == 0
);
/*
* La ligne V1 doit être conservée.
*/
assert(
strcmp(
evidence_count,
"1"
) == 0
);
assert(
strcmp(
internal_name,
"ancienne_capture.png"
) == 0
);
/*
* Le backfill V2 doit recopier name dans original_name.
*/
assert(
strcmp(
original_name,
"ancienne_capture.png"
) == 0
);
assert(
strcmp(
relative_path,
"01_Preuves_Originales/ancienne_capture.png"
) == 0
);
/*
* Une ancienne preuve nayant jamais é contrôlée reçoit UNKNOWN.
*/
assert(
strcmp(
integrity_status,
"0"
) == 0
);
/*
* Les quatre colonnes ne doivent exister quune fois chacune.
*/
assert(
strcmp(
v2_column_count,
"4"
) == 0
);
assert(
strcmp(
migration_index_count,
"1"
) == 0
);
assert(
strcmp(
migration_trigger_count,
"2"
) == 0
);
result = sqlite3_close(
database
);
assert(result == SQLITE_OK);
assert(
g_remove(
database_path
) == 0
);
assert(
g_rmdir(
temporary_directory
) == 0
);
g_free(
migration_trigger_count
);
g_free(
migration_index_count
);
g_free(
v2_column_count
);
g_free(
evidence_count
);
g_free(
integrity_status
);
g_free(
relative_path
);
g_free(
internal_name
);
g_free(
original_name
);
g_free(
schema_version
);
g_free(
database_path
);
g_free(
temporary_directory
);
}
/**
* @brief Vérifie quune migration V2 échouée est entièrement annulée.
*/
static void test_database_migration_rollback(void)
{
char *temporary_directory = NULL;
char *database_path = NULL;
char *schema_version = NULL;
char *original_name_column_count = NULL;
char *collected_at_column_count = NULL;
char *source_column_count = NULL;
char *integrity_status_column_count = NULL;
char *evidence_count = NULL;
char *integrity_result = NULL;
Database *database_context = NULL;
sqlite3 *database = NULL;
GError *error = NULL;
int result = SQLITE_ERROR;
temporary_directory =
g_dir_make_tmp(
"labfy-database-migration-rollback-test-XXXXXX",
&error
);
assert(temporary_directory != NULL);
assert(error == NULL);
database_path =
g_build_filename(
temporary_directory,
"Enquete.sqlite",
NULL
);
assert(database_path != NULL);
test_database_create_v1_database(
database_path
);
result = sqlite3_open_v2(
database_path,
&database,
SQLITE_OPEN_READWRITE,
NULL
);
assert(result == SQLITE_OK);
assert(database != NULL);
/*
* La migration V2 essaiera de créer un index portant ce nom.
* Le conflit doit provoquer un échec après les ALTER TABLE.
*/
test_database_execute_sql(
database,
"CREATE INDEX idx_preuves_imported_at "
"ON preuves(name);"
);
result = sqlite3_close(
database
);
assert(result == SQLITE_OK);
database = NULL;
database_context =
database_open(
database_path
);
assert(database_context != NULL);
assert(
!database_migrate_to_latest(
database_context
)
);
assert(
database_error_get_code(
database_context
) != DATABASE_ERROR_NONE
);
database_close(
database_context
);
result = sqlite3_open_v2(
database_path,
&database,
SQLITE_OPEN_READONLY,
NULL
);
assert(result == SQLITE_OK);
assert(database != NULL);
schema_version =
test_database_read_single_text(
database,
"SELECT value "
"FROM metadata "
"WHERE key = 'schema_version';"
);
original_name_column_count =
test_database_read_single_text(
database,
"SELECT CAST(COUNT(*) AS TEXT) "
"FROM pragma_table_info('preuves') "
"WHERE name = 'original_name';"
);
collected_at_column_count =
test_database_read_single_text(
database,
"SELECT CAST(COUNT(*) AS TEXT) "
"FROM pragma_table_info('preuves') "
"WHERE name = 'collected_at';"
);
source_column_count =
test_database_read_single_text(
database,
"SELECT CAST(COUNT(*) AS TEXT) "
"FROM pragma_table_info('preuves') "
"WHERE name = 'source';"
);
integrity_status_column_count =
test_database_read_single_text(
database,
"SELECT CAST(COUNT(*) AS TEXT) "
"FROM pragma_table_info('preuves') "
"WHERE name = 'integrity_status';"
);
evidence_count =
test_database_read_single_text(
database,
"SELECT CAST(COUNT(*) AS TEXT) "
"FROM preuves;"
);
integrity_result =
test_database_read_single_text(
database,
"PRAGMA integrity_check;"
);
/*
* La version ne doit pas avancer lorsque le SQL V2 échoue.
*/
assert(
strcmp(
schema_version,
"1"
) == 0
);
/*
* Les ALTER TABLE précédant lerreur doivent avoir é annulés.
*/
assert(
strcmp(
original_name_column_count,
"0"
) == 0
);
assert(
strcmp(
collected_at_column_count,
"0"
) == 0
);
assert(
strcmp(
source_column_count,
"0"
) == 0
);
assert(
strcmp(
integrity_status_column_count,
"0"
) == 0
);
/*
* La preuve V1 doit rester intacte.
*/
assert(
strcmp(
evidence_count,
"1"
) == 0
);
assert(
strcmp(
integrity_result,
"ok"
) == 0
);
result = sqlite3_close(
database
);
assert(result == SQLITE_OK);
assert(
g_remove(
database_path
) == 0
);
assert(
g_rmdir(
temporary_directory
) == 0
);
g_free(
integrity_result
);
g_free(
evidence_count
);
g_free(
integrity_status_column_count
);
g_free(
source_column_count
);
g_free(
collected_at_column_count
);
g_free(
original_name_column_count
);
g_free(
schema_version
);
g_free(
database_path
);
g_free(
temporary_directory
);
}
int main(void) int main(void)
{ {
test_database_initialize_valid_database(); test_database_initialize_valid_database();
test_database_initialize_rollback(); test_database_initialize_rollback();
test_database_migrate_v1_to_v2();
test_database_migration_rollback();
test_database_initialize_invalid_parameters(); test_database_initialize_invalid_parameters();
test_database_initialize_missing_parent(); test_database_initialize_missing_parent();

Binary file not shown.

BIN
tests/test_evidence_dao Executable file

Binary file not shown.

1773
tests/test_evidence_dao.c Normal file

File diff suppressed because it is too large Load diff

Binary file not shown.

View file

@ -6,7 +6,6 @@
#include "models/evidence_record.h" #include "models/evidence_record.h"
#include <glib.h> #include <glib.h>
#include <string.h>
#define TEST_EVIDENCE_IDENTIFIER \ #define TEST_EVIDENCE_IDENTIFIER \
"6e62b9af-2046-4efd-b3b9-29869f816951" "6e62b9af-2046-4efd-b3b9-29869f816951"

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -548,7 +548,7 @@ static void test_open_invalid_database(void)
test_assert_session_error( test_assert_session_error(
error, error,
INVESTIGATION_SESSION_ERROR_RECORD INVESTIGATION_SESSION_ERROR_DATABASE
); );
g_clear_error(&error); g_clear_error(&error);

Binary file not shown.

Binary file not shown.