feat(person): enrichir la création avec rôles et preuves

This commit is contained in:
grayTerminal-sh 2026-07-28 11:27:26 +02:00
parent db9be54a1a
commit dfe6afcfca
44 changed files with 2677 additions and 48 deletions

View file

@ -14,7 +14,7 @@ CFLAGS = -std=c17 \
-MP \ -MP \
$(shell $(PKG_CONFIG) --cflags gtk4 sqlite3) $(shell $(PKG_CONFIG) --cflags gtk4 sqlite3)
LDFLAGS = $(shell $(PKG_CONFIG) --libs gtk4 sqlite3) LDFLAGS = $(shell $(PKG_CONFIG) --libs gtk4 sqlite3) -ljpeg
TEST_CFLAGS = -std=c17 \ TEST_CFLAGS = -std=c17 \
-Wall \ -Wall \
@ -125,6 +125,13 @@ TEST_EVIDENCE_RECLASSIFICATION := tests/test_evidence_reclassification
TEST_SOCIAL_ACCOUNT_SERVICE := tests/test_social_account_service TEST_SOCIAL_ACCOUNT_SERVICE := tests/test_social_account_service
TEST_SOCIAL_PLATFORM := tests/test_social_platform TEST_SOCIAL_PLATFORM := tests/test_social_platform
TEST_PERSON_ENTITY_SERVICE := tests/test_person_entity_service TEST_PERSON_ENTITY_SERVICE := tests/test_person_entity_service
TEST_EVIDENCE_PREVIEW := tests/test_evidence_preview
TEST_EVIDENCE_SELECTION_MODEL := tests/test_evidence_selection_model
TEST_PERSON_CREATION_GUARD := tests/test_person_creation_guard
TEST_PERSON_DIALOG_LIFECYCLE := tests/test_person_dialog_lifecycle
TEST_CREATE_PERSON_DIALOG_GTK := tests/test_create_person_dialog_gtk
TEST_PERSON_CONFIRMATION_SUMMARY := tests/test_person_confirmation_summary
TEST_PERSON_ROLE_ASSIGNMENT_DAO := tests/test_person_role_assignment_dao
TEST_EML_ANALYZER := tests/test_eml_analyzer TEST_EML_ANALYZER := tests/test_eml_analyzer
TEST_EML_INTEGRATION := tests/test_eml_integration TEST_EML_INTEGRATION := tests/test_eml_integration
TEST_IBAN_ANALYZER := tests/test_iban_analyzer TEST_IBAN_ANALYZER := tests/test_iban_analyzer
@ -778,10 +785,12 @@ $(TEST_SOCIAL_PLATFORM): \
$(TEST_PERSON_ENTITY_SERVICE): \ $(TEST_PERSON_ENTITY_SERVICE): \
tests/test_person_entity_service.c \ tests/test_person_entity_service.c \
src/core/person_entity_service.c \ src/core/person_entity_service.c \
src/dao/person_role_assignment_dao.c \
src/dao/entity_dao.c \ src/dao/entity_dao.c \
src/dao/evidence_entity_dao.c \ src/dao/evidence_entity_dao.c \
src/models/entity_record.c \ src/models/entity_record.c \
src/models/evidence_observation.c \ src/models/evidence_observation.c \
src/models/person_role_assignment.c \
src/database/database.c \ src/database/database.c \
src/database/schema.c \ src/database/schema.c \
src/database/statement.c \ src/database/statement.c \
@ -789,6 +798,50 @@ $(TEST_PERSON_ENTITY_SERVICE): \
src/database/error.c src/database/error.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3 $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3
$(TEST_EVIDENCE_PREVIEW): tests/test_evidence_preview.c \
src/core/evidence_preview.c \
src/core/evidence_preview_task.c src/core/background_task.c \
src/core/task_manager.c \
src/core/evidence_integrity_verifier.c \
src/core/file_hash.c src/models/evidence_record.c
$(CC) $(CFLAGS) -Wpedantic $^ -o $@ $(LDFLAGS) -ljpeg
$(TEST_EVIDENCE_SELECTION_MODEL): tests/test_evidence_selection_model.c \
src/models/evidence_selection_model.c src/models/evidence_record.c
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ $(TEST_LDFLAGS)
$(TEST_PERSON_CREATION_GUARD): tests/test_person_creation_guard.c \
src/core/person_creation_guard.c
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ $(TEST_LDFLAGS)
$(TEST_PERSON_DIALOG_LIFECYCLE): tests/test_person_dialog_lifecycle.c \
src/core/person_dialog_lifecycle.c
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ $(TEST_LDFLAGS)
$(TEST_CREATE_PERSON_DIALOG_GTK): tests/test_create_person_dialog_gtk.c \
src/views/create_person_dialog.c src/core/person_dialog_lifecycle.c \
src/core/person_confirmation_summary.c \
src/core/evidence_preview_task.c src/core/evidence_preview.c \
src/core/evidence_integrity_verifier.c src/core/file_hash.c \
src/core/background_task.c src/core/task_manager.c \
src/models/evidence_selection_model.c src/models/evidence_record.c \
src/models/person_role_assignment.c
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
$(TEST_PERSON_CONFIRMATION_SUMMARY): \
tests/test_person_confirmation_summary.c \
src/core/person_confirmation_summary.c src/models/evidence_record.c
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ $(TEST_LDFLAGS)
$(TEST_PERSON_ROLE_ASSIGNMENT_DAO): \
tests/test_person_role_assignment_dao.c \
src/dao/person_role_assignment_dao.c src/models/person_role_assignment.c \
src/dao/entity_dao.c src/models/entity_record.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) -Wpedantic $^ -o $@ $(TEST_LDFLAGS) -lsqlite3
$(TEST_EML_ANALYZER): tests/test_eml_analyzer.c src/core/eml_analyzer.c $(TEST_EML_ANALYZER): tests/test_eml_analyzer.c src/core/eml_analyzer.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
@ -895,6 +948,13 @@ test: \
$(TEST_SOCIAL_ACCOUNT_SERVICE) \ $(TEST_SOCIAL_ACCOUNT_SERVICE) \
$(TEST_SOCIAL_PLATFORM) \ $(TEST_SOCIAL_PLATFORM) \
$(TEST_PERSON_ENTITY_SERVICE) \ $(TEST_PERSON_ENTITY_SERVICE) \
$(TEST_EVIDENCE_PREVIEW) \
$(TEST_EVIDENCE_SELECTION_MODEL) \
$(TEST_PERSON_CREATION_GUARD) \
$(TEST_PERSON_DIALOG_LIFECYCLE) \
$(TEST_CREATE_PERSON_DIALOG_GTK) \
$(TEST_PERSON_CONFIRMATION_SUMMARY) \
$(TEST_PERSON_ROLE_ASSIGNMENT_DAO) \
$(TEST_EML_ANALYZER) \ $(TEST_EML_ANALYZER) \
$(TEST_EML_INTEGRATION) \ $(TEST_EML_INTEGRATION) \
$(TEST_IBAN_ANALYZER) \ $(TEST_IBAN_ANALYZER) \
@ -969,6 +1029,13 @@ test: \
@$(TEST_SOCIAL_ACCOUNT_SERVICE) @$(TEST_SOCIAL_ACCOUNT_SERVICE)
@$(TEST_SOCIAL_PLATFORM) @$(TEST_SOCIAL_PLATFORM)
@$(TEST_PERSON_ENTITY_SERVICE) @$(TEST_PERSON_ENTITY_SERVICE)
@$(TEST_EVIDENCE_PREVIEW)
@$(TEST_EVIDENCE_SELECTION_MODEL)
@$(TEST_PERSON_CREATION_GUARD)
@$(TEST_PERSON_DIALOG_LIFECYCLE)
@$(TEST_CREATE_PERSON_DIALOG_GTK)
@$(TEST_PERSON_CONFIRMATION_SUMMARY)
@$(TEST_PERSON_ROLE_ASSIGNMENT_DAO)
@$(TEST_EML_ANALYZER) @$(TEST_EML_ANALYZER)
@$(TEST_EML_INTEGRATION) @$(TEST_EML_INTEGRATION)
@$(TEST_IBAN_ANALYZER) @$(TEST_IBAN_ANALYZER)
@ -1050,6 +1117,13 @@ clean:
$(TEST_SOCIAL_ACCOUNT_SERVICE) \ $(TEST_SOCIAL_ACCOUNT_SERVICE) \
$(TEST_SOCIAL_PLATFORM) \ $(TEST_SOCIAL_PLATFORM) \
$(TEST_PERSON_ENTITY_SERVICE) \ $(TEST_PERSON_ENTITY_SERVICE) \
$(TEST_EVIDENCE_PREVIEW) \
$(TEST_EVIDENCE_SELECTION_MODEL) \
$(TEST_PERSON_CREATION_GUARD) \
$(TEST_PERSON_DIALOG_LIFECYCLE) \
$(TEST_CREATE_PERSON_DIALOG_GTK) \
$(TEST_PERSON_CONFIRMATION_SUMMARY) \
$(TEST_PERSON_ROLE_ASSIGNMENT_DAO) \
$(TEST_EML_ANALYZER) \ $(TEST_EML_ANALYZER) \
$(TEST_EML_INTEGRATION) \ $(TEST_EML_INTEGRATION) \
$(TEST_EXTRACTION_DROP_SERVICE) \ $(TEST_EXTRACTION_DROP_SERVICE) \

View file

@ -238,3 +238,30 @@ CREATE INDEX IF NOT EXISTS idx_preuve_entite_sources_entity
ON preuve_entite_sources(entite_id); ON preuve_entite_sources(entite_id);
CREATE INDEX IF NOT EXISTS idx_preuve_entite_sources_source CREATE INDEX IF NOT EXISTS idx_preuve_entite_sources_source
ON preuve_entite_sources(source_kind, source_uuid); ON preuve_entite_sources(source_kind, source_uuid);
CREATE TABLE IF NOT EXISTS person_role_assignments
(
id TEXT PRIMARY KEY CHECK (length(trim(id)) > 0),
entity_id TEXT NOT NULL,
role_code TEXT NOT NULL CHECK (length(trim(role_code)) > 0),
evidence_id TEXT,
provenance_kind TEXT NOT NULL CHECK (
provenance_kind IN ('manual', 'legacy_manual')
),
confidence INTEGER CHECK (
confidence IS NULL OR confidence BETWEEN 0 AND 100
),
notes TEXT,
created_at TEXT NOT NULL CHECK (length(created_at) = 20),
updated_at TEXT NOT NULL CHECK (length(updated_at) = 20),
FOREIGN KEY (entity_id) REFERENCES entites(id) ON DELETE CASCADE,
FOREIGN KEY (evidence_id) REFERENCES preuves(id) ON DELETE SET NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_person_role_assignments_semantic
ON person_role_assignments(
entity_id, role_code, COALESCE(evidence_id, ''), provenance_kind
);
CREATE INDEX IF NOT EXISTS idx_person_role_assignments_entity
ON person_role_assignments(entity_id);
CREATE INDEX IF NOT EXISTS idx_person_role_assignments_evidence
ON person_role_assignments(evidence_id);

46
database/schema_v14.sql Normal file
View file

@ -0,0 +1,46 @@
/******************************************************************************
* Schéma SQLite V14 affectations contextuelles multiples des personnes.
*
* L'unicité porte sur (personne, rôle, preuve, provenance) : un même rôle
* peut donc être justifié par plusieurs preuves distinctes.
******************************************************************************/
CREATE TABLE person_role_assignments
(
id TEXT PRIMARY KEY CHECK (length(trim(id)) > 0),
entity_id TEXT NOT NULL,
role_code TEXT NOT NULL CHECK (length(trim(role_code)) > 0),
evidence_id TEXT,
provenance_kind TEXT NOT NULL CHECK (
provenance_kind IN ('manual', 'legacy_manual')
),
confidence INTEGER CHECK (
confidence IS NULL OR confidence BETWEEN 0 AND 100
),
notes TEXT,
created_at TEXT NOT NULL CHECK (length(created_at) = 20),
updated_at TEXT NOT NULL CHECK (length(updated_at) = 20),
FOREIGN KEY (entity_id) REFERENCES entites(id) ON DELETE CASCADE,
FOREIGN KEY (evidence_id) REFERENCES preuves(id) ON DELETE SET NULL
);
CREATE UNIQUE INDEX idx_person_role_assignments_semantic
ON person_role_assignments(
entity_id, role_code, COALESCE(evidence_id, ''), provenance_kind
);
CREATE INDEX idx_person_role_assignments_entity
ON person_role_assignments(entity_id);
CREATE INDEX idx_person_role_assignments_evidence
ON person_role_assignments(evidence_id);
/* Les codes historiques sont conservés littéralement : aucune équivalence
* approximative n'est appliquée pendant la migration. */
INSERT INTO person_role_assignments(
id, entity_id, role_code, evidence_id, provenance_kind,
confidence, notes, created_at, updated_at
)
SELECT
lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-4' ||
substr(lower(hex(randomblob(2))),2) || '-a' ||
substr(lower(hex(randomblob(2))),2) || '-' || lower(hex(randomblob(6))),
entity_id, role, NULL, 'legacy_manual', NULL, NULL, updated_at, updated_at
FROM person_roles;

View file

@ -2,7 +2,22 @@
> **Version :** 3.1 > **Version :** 3.1
> **Dernière mise à jour :** 2026-07-28 > **Dernière mise à jour :** 2026-07-28
> **Schéma SQLite courant :** V13 > **Schéma SQLite courant :** V14
## Personnes contextuelles — SQLite V14
`person_role_assignments` sépare les rôles contextuels des champs de lentité.
Une personne peut porter plusieurs rôles avec preuve facultative, provenance,
confiance et notes. Les anciens codes sont copiés littéralement depuis
`person_roles` avec la provenance `legacy_manual`. Le service crée lentité,
les rôles et le rattachement manuel dans une transaction unique.
Le cœur `EvidencePreview` vérifie lintégrité avant décodage PNG/JPEG, applique
des bornes et ne renvoie quun PNG mémoire réduit. Une `BackgroundTask`
travaille hors du thread GTK ; la texture est créée sur le contexte principal.
Le dialogue annule lancienne tâche et rejette les générations obsolètes.
Linterface conserve une génération de session et les chemins stables du
projet et de sa base. PDF, vidéo, EML avancé et OCR restent exclus.
> **Statut :** architecture courante > **Statut :** architecture courante
--- ---
@ -301,7 +316,7 @@ validation du chemin
création de l'arborescence création de l'arborescence
initialisation transactionnelle de SQLite V13 initialisation transactionnelle de SQLite V14
création de l'identité de l'enquête création de l'identité de l'enquête

View file

@ -1,5 +1,9 @@
# Dépendances externes # Dépendances externes
`libjpeg` est utilisée pour le décodage borné des aperçus JPEG du ticket #109.
PNG est décodé avec Cairo. Aucun outil documentaire externe nest lancé par
laperçu et aucun dérivé nest persisté.
Ce fichier recense les outils utilisés par LabFy Investigation et leur Ce fichier recense les outils utilisés par LabFy Investigation et leur
installation. Il sera complété à chaque intégration dun nouvel outil. installation. Il sera complété à chaque intégration dun nouvel outil.

View file

@ -405,16 +405,16 @@ tests/test_database.c
### 9.2 Nouvelle version de schéma ### 9.2 Nouvelle version de schéma
Pour créer une nouvelle version après V13, par exemple V14 : Pour créer une nouvelle version après V14, par exemple V15 :
1. ajouter `database/schema_v13.sql` ; 1. ajouter le nouveau fichier `database/schema_v15.sql` ;
2. déclarer et implémenter `schema_install_v13()` ; 2. déclarer et implémenter `schema_install_v13()` ;
3. ajouter `database_migrate_v12_to_v13()` ; 3. ajouter `database_migrate_v12_to_v13()` ;
4. raccorder la migration dans la boucle vers la version courante ; 4. raccorder la migration dans la boucle vers la version courante ;
5. mettre à jour les constantes de version ; 5. mettre à jour les constantes de version ;
6. installer V13 lors de la création d'une base neuve ; 6. installer V15 lors de la création d'une base neuve ;
7. adapter `schema_current.sql` si nécessaire ; 7. adapter `schema_current.sql` si nécessaire ;
8. ajouter une fixture V13 vers V14 ; 8. ajouter une fixture V14 vers V15 ;
La fixture V12 vers V13 vérifie le backfill `legacy_manual`. Les tests EML La fixture V12 vers V13 vérifie le backfill `legacy_manual`. Les tests EML
couvrent aussi le retrait isolé d'une source face à un rattachement manuel. couvrent aussi le retrait isolé d'une source face à un rattachement manuel.

View file

@ -2,7 +2,12 @@
> **Dernière mise à jour :** 2026-07-28 > **Dernière mise à jour :** 2026-07-28
> **État du projet :** développement actif > **État du projet :** développement actif
> **Schéma SQLite courant :** V13 > **Schéma SQLite courant :** V14
La tranche 1 du ticket #109 couvre les rôles contextuels multiples, la
sélection dune preuve existante, la création transactionnelle et la garde de
session. Import intégré, PDF, vidéo, EML avancé, OCR et documents didentité
restent hors périmètre.
> **Usage opérationnel :** non prêt pour la production > **Usage opérationnel :** non prêt pour la production
--- ---
@ -116,7 +121,7 @@ La branche `main` contient notamment :
- création, validation et ouverture d'enquêtes ; - création, validation et ouverture d'enquêtes ;
- session d'enquête remplaçable proprement ; - session d'enquête remplaçable proprement ;
- infrastructure SQLite et migrations jusqu'à V13 ; - infrastructure SQLite et migrations jusqu'à V14 ;
- couche Database, DAO et services métier ; - couche Database, DAO et services métier ;
- import de preuves avec copie contrôlée et SHA-256 ; - import de preuves avec copie contrôlée et SHA-256 ;
- vérification d'intégrité et reclassement des preuves ; - vérification d'intégrité et reclassement des preuves ;

View file

@ -0,0 +1,29 @@
# Test manuel — ticket #109, tranche 1
Utiliser uniquement une enquête de démonstration et des fichiers synthétiques
marqués `SPECIMEN`.
1. Cliquer sur Annuler à chacune des quatre étapes doit fermer uniquement
lassistant et laisser la fenêtre principale utilisable. Refaire le contrôle
avec la croix, puis pendant le chargement dun aperçu.
2. Ouvrir une enquête synthétique, puis demander lajout dune personne.
3. Vérifier les quatre étapes `Personne`, `Rôles`, `Preuve`, `Confirmation`.
4. Revenir en arrière : les champs, rôles multiples et la preuve choisie
doivent être conservés. Aucun rôle sensible ne doit être précoché.
5. Rechercher par nom, description et type, combiner avec le filtre métier,
puis revenir à `Tous les types`.
6. Choisir des PNG et JPEG synthétiques : vérifier chargement, aperçu et
métadonnées complètes, puis les états invalide et absent.
7. Sélectionner rapidement plusieurs preuves : seul le dernier aperçu doit
apparaître, puis fermer la fenêtre pendant un chargement.
8. Choisir zéro puis une preuve existante. Aucun import, OCR, PDF, vidéo ou
analyse EML avancée ne doit être proposé.
9. Confirmer et vérifier que personne, rôles, rattachement et source `manual`
apparaissent ensemble après actualisation.
10. Changer denquête pendant un aperçu puis avant confirmation : le résultat
tardif et la création doivent être refusés sans écriture ni rafraîchissement.
Laperçu de tranche 1 accepte uniquement PNG/JPEG intègres. Il refuse les
fichiers absents, modifiés, corrompus ou supérieurs aux limites de 25 Mio,
12 000 pixels par côté et 40 millions de pixels. Le rendu mémoire est borné à
1 024 × 1 024 et aucun dérivé nest persisté.

View file

@ -0,0 +1,36 @@
#ifndef LABFY_INVESTIGATION_EVIDENCE_PREVIEW_H
#define LABFY_INVESTIGATION_EVIDENCE_PREVIEW_H
#include <gio/gio.h>
#include <glib.h>
G_BEGIN_DECLS
#define EVIDENCE_PREVIEW_MAX_FILE_BYTES (25U * 1024U * 1024U)
#define EVIDENCE_PREVIEW_MAX_DIMENSION 12000
#define EVIDENCE_PREVIEW_MAX_PIXELS 40000000U
#define EVIDENCE_PREVIEW_MAX_EDGE 1024
typedef struct {
char *investigation_root_path;
char *evidence_identifier;
char *relative_path;
char *expected_sha256;
char *mime_type;
guint64 request_generation;
} EvidencePreviewRequest;
typedef struct {
char *evidence_identifier;
guint64 request_generation;
GBytes *png_bytes;
gint width;
gint height;
} EvidencePreviewResult;
EvidencePreviewRequest *evidence_preview_request_new(const char *root,
const char *evidence_identifier, const char *relative_path,
const char *sha256, const char *mime_type, guint64 generation);
void evidence_preview_request_free(EvidencePreviewRequest *request);
EvidencePreviewResult *evidence_preview_load(
const EvidencePreviewRequest *request, GCancellable *cancellable,
GError **error);
void evidence_preview_result_free(EvidencePreviewResult *result);
gboolean evidence_preview_result_matches(const EvidencePreviewResult *result,
const char *evidence_identifier, guint64 generation);
G_END_DECLS
#endif

View file

@ -0,0 +1,12 @@
#ifndef LABFY_INVESTIGATION_EVIDENCE_PREVIEW_TASK_H
#define LABFY_INVESTIGATION_EVIDENCE_PREVIEW_TASK_H
#include "core/background_task.h"
#include "core/evidence_preview.h"
#include "core/task_manager.h"
G_BEGIN_DECLS
BackgroundTask *evidence_preview_task_start(TaskManager *manager,
const EvidencePreviewRequest *request,
BackgroundTaskCompletionCallback callback, gpointer callback_data,
GDestroyNotify callback_data_destroy, GError **error);
G_END_DECLS
#endif

View file

@ -0,0 +1,16 @@
#ifndef LABFY_INVESTIGATION_PERSON_CONFIRMATION_SUMMARY_H
#define LABFY_INVESTIGATION_PERSON_CONFIRMATION_SUMMARY_H
#include "models/evidence_record.h"
#include <glib.h>
G_BEGIN_DECLS
char *person_confirmation_summary_build(const char *designation,
const char *declared_name, const char *pseudonym,
const char *identification_status, gint confidence, const char *notes,
const GPtrArray *role_labels, const EvidenceRecord *evidence);
G_END_DECLS
#endif

View file

@ -0,0 +1,13 @@
#ifndef LABFY_INVESTIGATION_PERSON_CREATION_GUARD_H
#define LABFY_INVESTIGATION_PERSON_CREATION_GUARD_H
#include <glib.h>
G_BEGIN_DECLS
typedef struct PersonCreationGuard PersonCreationGuard;
PersonCreationGuard *person_creation_guard_new(guint64 generation,
const char *root_path, const char *database_path);
void person_creation_guard_free(PersonCreationGuard *guard);
gboolean person_creation_guard_matches(const PersonCreationGuard *guard,
gboolean session_is_open, guint64 generation, const char *root_path,
const char *database_path);
G_END_DECLS
#endif

View file

@ -0,0 +1,29 @@
#ifndef LABFY_INVESTIGATION_PERSON_DIALOG_LIFECYCLE_H
#define LABFY_INVESTIGATION_PERSON_DIALOG_LIFECYCLE_H
#include <glib.h>
G_BEGIN_DECLS
typedef struct PersonDialogLifecycle PersonDialogLifecycle;
PersonDialogLifecycle *person_dialog_lifecycle_new(void);
void person_dialog_lifecycle_free(PersonDialogLifecycle *lifecycle);
guint64 person_dialog_lifecycle_begin_preview(
PersonDialogLifecycle *lifecycle);
gboolean person_dialog_lifecycle_cancel(PersonDialogLifecycle *lifecycle);
gboolean person_dialog_lifecycle_complete(PersonDialogLifecycle *lifecycle);
gboolean person_dialog_lifecycle_accepts_preview(
const PersonDialogLifecycle *lifecycle, guint64 generation);
gboolean person_dialog_lifecycle_is_cancelled(
const PersonDialogLifecycle *lifecycle);
gboolean person_dialog_lifecycle_is_finished(
const PersonDialogLifecycle *lifecycle);
gboolean person_dialog_lifecycle_has_preview(
const PersonDialogLifecycle *lifecycle);
guint64 person_dialog_lifecycle_get_generation(
const PersonDialogLifecycle *lifecycle);
G_END_DECLS
#endif

View file

@ -6,6 +6,7 @@
#define LABFY_INVESTIGATION_PERSON_ENTITY_SERVICE_H #define LABFY_INVESTIGATION_PERSON_ENTITY_SERVICE_H
#include "database/database.h" #include "database/database.h"
#include "models/entity_record.h" #include "models/entity_record.h"
#include "models/person_role_assignment.h"
#include <glib.h> #include <glib.h>
G_BEGIN_DECLS G_BEGIN_DECLS
/** @brief Données factuelles utilisées pour créer une personne. */ /** @brief Données factuelles utilisées pour créer une personne. */
@ -18,6 +19,8 @@ typedef struct
const char *notes; const char *notes;
gint confidence; gint confidence;
const char *evidence_identifier; const char *evidence_identifier;
/** PersonRoleAssignmentInput* empruntés, facultatif. */
const GPtrArray *role_assignments;
} PersonEntityInput; } PersonEntityInput;
/** /**
* @brief Crée une personne et son éventuelle liaison à une preuve. * @brief Crée une personne et son éventuelle liaison à une preuve.

View file

@ -0,0 +1,20 @@
#ifndef LABFY_INVESTIGATION_PERSON_ROLE_ASSIGNMENT_DAO_H
#define LABFY_INVESTIGATION_PERSON_ROLE_ASSIGNMENT_DAO_H
#include "database/database.h"
#include "models/person_role_assignment.h"
#include <glib.h>
G_BEGIN_DECLS
typedef struct PersonRoleAssignmentDao PersonRoleAssignmentDao;
PersonRoleAssignmentDao *person_role_assignment_dao_new(
Database *database, GError **error);
void person_role_assignment_dao_free(PersonRoleAssignmentDao *dao);
gboolean person_role_assignment_dao_insert(PersonRoleAssignmentDao *dao,
const char *entity_identifier, const PersonRoleAssignmentInput *input,
GError **error);
gboolean person_role_assignment_dao_insert_all(PersonRoleAssignmentDao *dao,
const char *entity_identifier, const GPtrArray *inputs, GError **error);
GPtrArray *person_role_assignment_dao_list_by_entity(
PersonRoleAssignmentDao *dao, const char *entity_identifier,
GError **error);
G_END_DECLS
#endif

View file

@ -94,6 +94,7 @@ bool schema_install_v10(Database *database);
bool schema_install_v11(Database *database); bool schema_install_v11(Database *database);
bool schema_install_v12(Database *database); bool schema_install_v12(Database *database);
bool schema_install_v13(Database *database); bool schema_install_v13(Database *database);
bool schema_install_v14(Database *database);
/** /**
* @brief Garantit la présence des extensions du schéma courant V2. * @brief Garantit la présence des extensions du schéma courant V2.

View file

@ -147,6 +147,13 @@ EvidenceRecord *evidence_record_new(
GError **error GError **error
); );
/**
* @brief Copie intégralement une preuve, métadonnées daffichage incluses.
*
* @return Nouvelle preuve possédée par lappelant, ou NULL.
*/
EvidenceRecord *evidence_record_copy(const EvidenceRecord *evidence_record);
/** /**
* @brief Libère un modèle EvidenceRecord. * @brief Libère un modèle EvidenceRecord.
* *
@ -203,6 +210,16 @@ const char *evidence_record_get_type_identifier(
const EvidenceRecord *evidence_record const EvidenceRecord *evidence_record
); );
/** @brief Enrichit les métadonnées d'affichage copiées par le modèle. */
void evidence_record_set_display_metadata(EvidenceRecord *evidence_record,
const char *type_label, const char *mime_type);
/** @brief Retourne le libellé métier du type, ou son code à défaut. */
const char *evidence_record_get_type_label(
const EvidenceRecord *evidence_record);
/** @brief Retourne le type MIME enregistré, éventuellement NULL. */
const char *evidence_record_get_mime_type(
const EvidenceRecord *evidence_record);
/** /**
* @brief Retourne la taille observée de la preuve. * @brief Retourne la taille observée de la preuve.
* *

View file

@ -0,0 +1,23 @@
#ifndef LABFY_INVESTIGATION_EVIDENCE_SELECTION_MODEL_H
#define LABFY_INVESTIGATION_EVIDENCE_SELECTION_MODEL_H
#include "models/evidence_record.h"
#include <glib.h>
G_BEGIN_DECLS
typedef struct EvidenceSelectionModel EvidenceSelectionModel;
/** Crée un modèle possédant des copies profondes des preuves fournies. */
EvidenceSelectionModel *evidence_selection_model_new(const GPtrArray *records);
void evidence_selection_model_free(EvidenceSelectionModel *model);
void evidence_selection_model_set_query(EvidenceSelectionModel *model,
const char *query);
void evidence_selection_model_set_type(EvidenceSelectionModel *model,
const char *type_code);
GPtrArray *evidence_selection_model_list_visible(
const EvidenceSelectionModel *model);
gboolean evidence_selection_model_select(EvidenceSelectionModel *model,
const char *identifier);
const EvidenceRecord *evidence_selection_model_get_selected(
const EvidenceSelectionModel *model);
GPtrArray *evidence_selection_model_list_type_codes(
const EvidenceSelectionModel *model);
G_END_DECLS
#endif

View file

@ -0,0 +1,35 @@
#ifndef LABFY_INVESTIGATION_PERSON_ROLE_ASSIGNMENT_H
#define LABFY_INVESTIGATION_PERSON_ROLE_ASSIGNMENT_H
#include <glib.h>
G_BEGIN_DECLS
typedef struct
{
char *role_code;
char *evidence_identifier;
char *provenance_kind;
gboolean has_confidence;
gint confidence;
char *notes;
} PersonRoleAssignmentInput;
typedef struct
{
char *identifier;
char *entity_identifier;
PersonRoleAssignmentInput assignment;
char *created_at;
char *updated_at;
} PersonRoleAssignmentRecord;
gboolean person_role_assignment_role_is_known(const char *role_code);
const char *person_role_assignment_role_label(const char *role_code);
gboolean person_role_assignment_input_is_valid(
const PersonRoleAssignmentInput *input);
PersonRoleAssignmentInput *person_role_assignment_input_copy(
const PersonRoleAssignmentInput *input);
void person_role_assignment_input_free(PersonRoleAssignmentInput *input);
void person_role_assignment_record_free(PersonRoleAssignmentRecord *record);
G_END_DECLS
#endif

View file

@ -6,6 +6,7 @@
#define LABFY_INVESTIGATION_CREATE_PERSON_DIALOG_H #define LABFY_INVESTIGATION_CREATE_PERSON_DIALOG_H
#include "core/person_entity_service.h" #include "core/person_entity_service.h"
#include "models/evidence_record.h" #include "models/evidence_record.h"
#include "core/task_manager.h"
#include <gtk/gtk.h> #include <gtk/gtk.h>
G_BEGIN_DECLS G_BEGIN_DECLS
/** @brief Résultat opaque possédé par le callback. */ /** @brief Résultat opaque possédé par le callback. */
@ -13,6 +14,7 @@ typedef struct CreatePersonDialogResult CreatePersonDialogResult;
/** @brief Callback appelé après validation ou annulation. */ /** @brief Callback appelé après validation ou annulation. */
typedef void (*CreatePersonDialogCallback)(CreatePersonDialogResult *result, typedef void (*CreatePersonDialogCallback)(CreatePersonDialogResult *result,
gpointer user_data); gpointer user_data);
typedef gboolean (*CreatePersonDialogSessionCheck)(gpointer user_data);
/** /**
* @brief Présente le formulaire de création d'une personne. * @brief Présente le formulaire de création d'une personne.
* @param parent Fenêtre parente. * @param parent Fenêtre parente.
@ -22,8 +24,10 @@ typedef void (*CreatePersonDialogCallback)(CreatePersonDialogResult *result,
* @return TRUE si le dialogue a é présenté. * @return TRUE si le dialogue a é présenté.
*/ */
gboolean create_person_dialog_present(GtkWindow *parent, gboolean create_person_dialog_present(GtkWindow *parent,
const GPtrArray *evidence_records, CreatePersonDialogCallback callback, const GPtrArray *evidence_records, const char *investigation_root_path,
gpointer user_data); TaskManager *task_manager, CreatePersonDialogSessionCheck session_check,
CreatePersonDialogCallback callback, gpointer user_data,
GDestroyNotify user_data_destroy);
/** @brief Libère un résultat. */ /** @brief Libère un résultat. */
void create_person_dialog_result_free(CreatePersonDialogResult *result); void create_person_dialog_result_free(CreatePersonDialogResult *result);
/** @brief Retourne les données empruntées du résultat. */ /** @brief Retourne les données empruntées du résultat. */

View file

@ -56,6 +56,7 @@
#include "core/relation_service.h" #include "core/relation_service.h"
#include "core/social_account_service.h" #include "core/social_account_service.h"
#include "core/person_entity_service.h" #include "core/person_entity_service.h"
#include "core/person_creation_guard.h"
#include "core/eml_processing.h" #include "core/eml_processing.h"
#include "core/eml_integration.h" #include "core/eml_integration.h"
#include "core/eml_pipeline_task.h" #include "core/eml_pipeline_task.h"
@ -142,6 +143,7 @@ struct Application
InvestigationGraphModel *graph_model; InvestigationGraphModel *graph_model;
InvestigationGraphLayout *graph_layout; InvestigationGraphLayout *graph_layout;
guint64 graph_load_generation; guint64 graph_load_generation;
guint64 session_generation;
gboolean graph_viewport_restored; gboolean graph_viewport_restored;
guint graph_viewport_save_source_id; guint graph_viewport_save_source_id;
@ -150,6 +152,20 @@ struct Application
char *pending_entity_selection_identifier; char *pending_entity_selection_identifier;
}; };
typedef struct
{
Application *application;
PersonCreationGuard *guard;
} ApplicationPersonDialogContext;
static void application_person_dialog_context_free(
ApplicationPersonDialogContext *context)
{
if (context == NULL) return;
person_creation_guard_free(context->guard);
g_free(context);
}
static void application_on_evidence_selected( static void application_on_evidence_selected(
const char *evidence_identifier, gpointer user_data); const char *evidence_identifier, gpointer user_data);
@ -2726,6 +2742,7 @@ static gboolean application_install_session(
application->tree_model = new_tree_model; application->tree_model = new_tree_model;
application->session = new_session; application->session = new_session;
application->session_generation++;
application->graph_viewport_restored = FALSE; application->graph_viewport_restored = FALSE;
main_window_set_tree_model( main_window_set_tree_model(
@ -5380,13 +5397,25 @@ static void application_on_add_social_account_requested(gpointer user_data)
static void application_on_person_completed( static void application_on_person_completed(
CreatePersonDialogResult *result, gpointer user_data) CreatePersonDialogResult *result, gpointer user_data)
{ {
Application *application = user_data; ApplicationPersonDialogContext *context = user_data;
const PersonEntityInput *input = create_person_dialog_result_get_input(result); Application *application = context != NULL ? context->application : NULL;
const PersonEntityInput *input = NULL;
const InvestigationProject *project = NULL; const InvestigationProject *project = NULL;
GError *error = NULL; GError *error = NULL;
char *identifier = NULL; char *identifier = NULL;
if (result == NULL || application == NULL || application->session == NULL) const char *active_root = NULL, *active_database = NULL;
{ create_person_dialog_result_free(result); return; } if (result == NULL) {
return;
}
input = create_person_dialog_result_get_input(result);
if (application == NULL || application->session == NULL)
goto changed_session;
project = investigation_session_get_project(application->session);
active_root = investigation_project_get_root_path(project);
active_database = investigation_project_get_database_path(project);
if (!person_creation_guard_matches(context->guard, TRUE,
application->session_generation, active_root, active_database))
goto changed_session;
if (!person_entity_service_create( if (!person_entity_service_create(
investigation_session_get_database(application->session), investigation_session_get_database(application->session),
input, &identifier, &error)) input, &identifier, &error))
@ -5402,6 +5431,24 @@ static void application_on_person_completed(
} }
g_clear_error(&error); g_free(identifier); g_clear_error(&error); g_free(identifier);
create_person_dialog_result_free(result); create_person_dialog_result_free(result);
return;
changed_session:
application_present_error(application, "Personne non créée",
"L'enquête active a changé depuis l'ouverture de l'assistant.");
create_person_dialog_result_free(result);
}
static gboolean application_person_dialog_session_matches(gpointer user_data)
{
ApplicationPersonDialogContext *context = user_data;
Application *application = context != NULL ? context->application : NULL;
const InvestigationProject *project;
if (application == NULL || application->session == NULL) return FALSE;
project = investigation_session_get_project(application->session);
return person_creation_guard_matches(context->guard, TRUE,
application->session_generation,
investigation_project_get_root_path(project),
investigation_project_get_database_path(project));
} }
/** @brief Charge les preuves puis ouvre le formulaire d'une personne. */ /** @brief Charge les preuves puis ouvre le formulaire d'une personne. */
@ -5410,6 +5457,8 @@ static void application_on_add_person_requested(gpointer user_data)
Application *application = user_data; Application *application = user_data;
EvidenceDao *dao = NULL; EvidenceDao *dao = NULL;
GPtrArray *records = NULL; GPtrArray *records = NULL;
ApplicationPersonDialogContext *context = NULL;
const InvestigationProject *project = NULL;
GError *error = NULL; GError *error = NULL;
if (application == NULL || application->session == NULL) return; if (application == NULL || application->session == NULL) return;
dao = evidence_dao_new( dao = evidence_dao_new(
@ -5418,9 +5467,21 @@ static void application_on_add_person_requested(gpointer user_data)
if (records == NULL) if (records == NULL)
application_present_error(application, "Formulaire indisponible", application_present_error(application, "Formulaire indisponible",
error != NULL ? error->message : "Impossible de charger les preuves."); error != NULL ? error->message : "Impossible de charger les preuves.");
else else {
project = investigation_session_get_project(application->session);
context = g_new0(ApplicationPersonDialogContext, 1);
context->application = application;
context->guard = person_creation_guard_new(
application->session_generation,
investigation_project_get_root_path(project),
investigation_project_get_database_path(project));
create_person_dialog_present(main_window_get_window(application->main_window), create_person_dialog_present(main_window_get_window(application->main_window),
records, application_on_person_completed, application); records, investigation_project_get_root_path(project),
application->task_manager,
application_person_dialog_session_matches,
application_on_person_completed, context,
(GDestroyNotify) application_person_dialog_context_free);
}
g_clear_pointer(&records, g_ptr_array_unref); g_clear_pointer(&records, g_ptr_array_unref);
evidence_dao_free(dao); g_clear_error(&error); evidence_dao_free(dao); g_clear_error(&error);
} }

213
src/core/evidence_preview.c Normal file
View file

@ -0,0 +1,213 @@
#include "core/evidence_preview.h"
#include "core/evidence_integrity_verifier.h"
#include <cairo.h>
#include <stdio.h>
#include <jpeglib.h>
#include <setjmp.h>
static GQuark preview_error(void)
{ return g_quark_from_static_string("evidence-preview-error"); }
typedef struct {
struct jpeg_error_mgr base;
jmp_buf jump;
} PreviewJpegError;
static void jpeg_failed(j_common_ptr info)
{
PreviewJpegError *error = (PreviewJpegError *) info->err;
longjmp(error->jump, 1);
}
static cairo_status_t write_png(void *closure,
const unsigned char *data, unsigned int length)
{
g_byte_array_append(closure, data, length);
return CAIRO_STATUS_SUCCESS;
}
static gboolean surface_to_result(cairo_surface_t *source, gint source_width,
gint source_height, const EvidencePreviewRequest *request,
EvidencePreviewResult **out_result, GError **error)
{
double scale = MIN(1.0, MIN(
(double) EVIDENCE_PREVIEW_MAX_EDGE / source_width,
(double) EVIDENCE_PREVIEW_MAX_EDGE / source_height));
gint width = MAX(1, (gint) (source_width * scale));
gint height = MAX(1, (gint) (source_height * scale));
cairo_surface_t *target = cairo_image_surface_create(
CAIRO_FORMAT_ARGB32, width, height);
cairo_t *cr = cairo_create(target);
GByteArray *bytes = g_byte_array_new();
cairo_scale(cr, scale, scale);
cairo_set_source_surface(cr, source, 0, 0);
cairo_paint(cr);
cairo_destroy(cr);
if (cairo_surface_write_to_png_stream(target, write_png, bytes) !=
CAIRO_STATUS_SUCCESS) {
g_set_error_literal(error, preview_error(), 4,
"Impossible de produire l'aperçu en mémoire.");
g_byte_array_unref(bytes); cairo_surface_destroy(target);
return FALSE;
}
cairo_surface_destroy(target);
*out_result = g_new0(EvidencePreviewResult, 1);
(*out_result)->evidence_identifier =
g_strdup(request->evidence_identifier);
(*out_result)->request_generation = request->request_generation;
(*out_result)->width = width; (*out_result)->height = height;
(*out_result)->png_bytes = g_byte_array_free_to_bytes(bytes);
return TRUE;
}
EvidencePreviewRequest *evidence_preview_request_new(const char *root,
const char *evidence_identifier, const char *relative_path,
const char *sha256, const char *mime_type, guint64 generation)
{
if (root == NULL || !g_uuid_string_is_valid(evidence_identifier) ||
relative_path == NULL || sha256 == NULL || mime_type == NULL)
return NULL;
EvidencePreviewRequest *r = g_new0(EvidencePreviewRequest, 1);
r->investigation_root_path = g_strdup(root);
r->evidence_identifier = g_strdup(evidence_identifier);
r->relative_path = g_strdup(relative_path);
r->expected_sha256 = g_strdup(sha256);
r->mime_type = g_strdup(mime_type);
r->request_generation = generation;
return r;
}
void evidence_preview_request_free(EvidencePreviewRequest *r)
{
if (r == NULL) return;
g_free(r->investigation_root_path); g_free(r->evidence_identifier);
g_free(r->relative_path); g_free(r->expected_sha256);
g_free(r->mime_type); g_free(r);
}
void evidence_preview_result_free(EvidencePreviewResult *r)
{
if (r == NULL) return;
g_free(r->evidence_identifier);
g_clear_pointer(&r->png_bytes, g_bytes_unref);
g_free(r);
}
gboolean evidence_preview_result_matches(const EvidencePreviewResult *r,
const char *identifier, guint64 generation)
{
return r != NULL && r->request_generation == generation &&
g_strcmp0(r->evidence_identifier, identifier) == 0;
}
EvidencePreviewResult *evidence_preview_load(
const EvidencePreviewRequest *request, GCancellable *cancellable,
GError **error)
{
EvidenceIntegrityVerificationResult *verification = NULL;
EvidencePreviewResult *result = NULL;
char *path = NULL;
gint width = 0, height = 0;
if (request == NULL ||
!(g_str_equal(request->mime_type, "image/png") ||
g_str_equal(request->mime_type, "image/jpeg"))) {
g_set_error_literal(error, preview_error(), 1,
"Aperçu indisponible pour ce format.");
return NULL;
}
verification = evidence_integrity_verifier_verify(
request->investigation_root_path, request->relative_path,
request->expected_sha256, cancellable, error);
if (verification == NULL) return NULL;
if (evidence_integrity_verification_result_get_status(verification) !=
EVIDENCE_INTEGRITY_STATUS_VALID) {
g_set_error_literal(error, preview_error(), 2,
"Aperçu refusé : l'intégrité de la preuve est invalide.");
goto cleanup;
}
if (evidence_integrity_verification_result_get_size_bytes(verification) >
EVIDENCE_PREVIEW_MAX_FILE_BYTES) {
g_set_error_literal(error, preview_error(), 3,
"L'image dépasse la taille maximale autorisée.");
goto cleanup;
}
if (cancellable != NULL &&
g_cancellable_set_error_if_cancelled(cancellable, error)) goto cleanup;
path = g_build_filename(request->investigation_root_path,
request->relative_path, NULL);
if (g_str_equal(request->mime_type, "image/png")) {
cairo_surface_t *surface = cairo_image_surface_create_from_png(path);
if (cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS) {
cairo_surface_destroy(surface);
g_set_error_literal(error, preview_error(), 4,
"Le fichier image est illisible ou corrompu.");
goto cleanup;
}
width = cairo_image_surface_get_width(surface);
height = cairo_image_surface_get_height(surface);
if (width > EVIDENCE_PREVIEW_MAX_DIMENSION ||
height > EVIDENCE_PREVIEW_MAX_DIMENSION ||
(guint64) width * (guint64) height > EVIDENCE_PREVIEW_MAX_PIXELS) {
g_set_error_literal(error, preview_error(), 5,
"Les dimensions de l'image dépassent les limites sûres.");
cairo_surface_destroy(surface);
goto cleanup;
}
surface_to_result(surface, width, height, request, &result, error);
cairo_surface_destroy(surface);
goto cleanup;
}
FILE *jpeg_file = fopen(path, "rb");
struct jpeg_decompress_struct jpeg = {0};
PreviewJpegError jpeg_error;
cairo_surface_t *surface = NULL;
if (jpeg_file == NULL) {
g_set_error_literal(error, preview_error(), 4,
"Le fichier image est illisible ou corrompu.");
goto cleanup;
}
jpeg.err = jpeg_std_error(&jpeg_error.base);
jpeg_error.base.error_exit = jpeg_failed;
if (setjmp(jpeg_error.jump) != 0) {
jpeg_destroy_decompress(&jpeg); fclose(jpeg_file);
if (surface != NULL) cairo_surface_destroy(surface);
g_set_error_literal(error, preview_error(), 4,
"Le fichier JPEG est illisible ou corrompu.");
goto cleanup;
}
jpeg_create_decompress(&jpeg); jpeg_stdio_src(&jpeg, jpeg_file);
jpeg_read_header(&jpeg, TRUE);
width = (gint) jpeg.image_width; height = (gint) jpeg.image_height;
if (width > EVIDENCE_PREVIEW_MAX_DIMENSION ||
height > EVIDENCE_PREVIEW_MAX_DIMENSION ||
(guint64) width * (guint64) height > EVIDENCE_PREVIEW_MAX_PIXELS) {
g_set_error_literal(error, preview_error(), 5,
"Les dimensions de l'image dépassent les limites sûres.");
jpeg_destroy_decompress(&jpeg); fclose(jpeg_file);
goto cleanup;
}
jpeg.out_color_space = JCS_RGB;
jpeg_start_decompress(&jpeg);
surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24, width, height);
unsigned char *pixels = cairo_image_surface_get_data(surface);
gint stride = cairo_image_surface_get_stride(surface);
JSAMPARRAY row = (*jpeg.mem->alloc_sarray)(
(j_common_ptr) &jpeg, JPOOL_IMAGE, (JDIMENSION) width * 3U, 1);
while (jpeg.output_scanline < jpeg.output_height) {
jpeg_read_scanlines(&jpeg, row, 1);
guint y = jpeg.output_scanline - 1;
guint32 *destination = (guint32 *) (pixels + y * stride);
for (gint x = 0; x < width; x++)
destination[x] = ((guint32) row[0][x * 3] << 16) |
((guint32) row[0][x * 3 + 1] << 8) | row[0][x * 3 + 2];
if (cancellable != NULL && g_cancellable_is_cancelled(cancellable))
break;
}
cairo_surface_mark_dirty(surface);
jpeg_finish_decompress(&jpeg); jpeg_destroy_decompress(&jpeg);
fclose(jpeg_file);
if (cancellable != NULL &&
g_cancellable_set_error_if_cancelled(cancellable, error)) {
cairo_surface_destroy(surface); goto cleanup;
}
surface_to_result(surface, width, height, request, &result, error);
cairo_surface_destroy(surface);
cleanup:
g_free(path);
evidence_integrity_verification_result_free(verification);
return result;
}

View file

@ -0,0 +1,37 @@
#include "core/evidence_preview_task.h"
static EvidencePreviewRequest *copy_request(const EvidencePreviewRequest *r)
{
return evidence_preview_request_new(r->investigation_root_path,
r->evidence_identifier, r->relative_path, r->expected_sha256,
r->mime_type, r->request_generation);
}
static gboolean worker(BackgroundTask *task, GCancellable *cancellable,
gpointer data, gpointer *result, GError **error)
{
(void) task;
*result = evidence_preview_load(data, cancellable, error);
return *result != NULL;
}
BackgroundTask *evidence_preview_task_start(TaskManager *manager,
const EvidencePreviewRequest *request,
BackgroundTaskCompletionCallback callback, gpointer callback_data,
GDestroyNotify callback_data_destroy, GError **error)
{
BackgroundTask *task = NULL;
EvidencePreviewRequest *copy = NULL;
if (manager == NULL || request == NULL) return NULL;
task = background_task_new("Aperçu de la preuve");
copy = copy_request(request);
if (task == NULL || copy == NULL ||
!task_manager_add(manager, task, error) ||
!background_task_start(task, worker, copy,
(GDestroyNotify) evidence_preview_request_free,
(GDestroyNotify) evidence_preview_result_free,
callback, callback_data, callback_data_destroy, error)) {
evidence_preview_request_free(copy);
background_task_unref(task);
return NULL;
}
return task;
}

View file

@ -0,0 +1,78 @@
#include "core/person_confirmation_summary.h"
static const char *optional(const char *value, const char *fallback)
{
return value != NULL && value[0] != '\0' ? value : fallback;
}
static const char *integrity(EvidenceIntegrityStatus status)
{
switch (status) {
case EVIDENCE_INTEGRITY_STATUS_VALID: return "Valide";
case EVIDENCE_INTEGRITY_STATUS_MISSING: return "Fichier absent";
case EVIDENCE_INTEGRITY_STATUS_MODIFIED: return "Invalide";
case EVIDENCE_INTEGRITY_STATUS_ERROR: return "Erreur";
default: return "Non vérifiée";
}
}
char *person_confirmation_summary_build(const char *designation,
const char *declared_name, const char *pseudonym,
const char *identification_status, gint confidence, const char *notes,
const GPtrArray *role_labels, const EvidenceRecord *evidence)
{
GString *summary = g_string_new(NULL);
char *size = NULL, *short_sha = NULL;
g_string_append_printf(summary,
"PERSONNE\nDésignation : %s\nNom déclaré : %s\nPseudonyme : %s\n"
"Statut didentification : %s\nConfiance : %d %%\n"
"Notes factuelles : %s\n\nRÔLES\n",
optional(designation, "Non renseignée"),
optional(declared_name, "Non renseigné"),
optional(pseudonym, "Non renseigné"),
optional(identification_status, "Non renseigné"),
confidence, optional(notes, "Aucune"));
if (role_labels == NULL || role_labels->len == 0)
g_string_append(summary, "Aucun rôle sélectionné\n");
else
for (guint i = 0; i < role_labels->len; i++)
g_string_append_printf(summary, "• %s — preuve : %s\n",
optional(g_ptr_array_index((GPtrArray *) role_labels, i),
"Rôle non renseigné"),
evidence != NULL
? optional(evidence_record_get_original_name(evidence),
"Non renseignée")
: "Aucune");
g_string_append(summary, "\nPREUVE PRINCIPALE\n");
if (evidence == NULL)
g_string_append(summary, "Aucune preuve principale associée.\n");
else {
size = g_format_size(evidence_record_get_size_bytes(evidence));
short_sha = g_strndup(evidence_record_get_sha256(evidence), 12);
g_string_append_printf(summary,
"Nom original : %s\nType métier : %s\nType MIME : %s\n"
"Taille : %s\nDate dimport : %s\nSHA-256 : %s…\n"
"Intégrité : %s\nDescription : %s\n",
optional(evidence_record_get_original_name(evidence),
"Non renseigné"),
optional(evidence_record_get_type_label(evidence),
evidence_record_get_type_identifier(evidence)),
optional(evidence_record_get_mime_type(evidence),
"Non renseigné"),
size, optional(evidence_record_get_imported_at(evidence),
"Non renseignée"),
optional(short_sha, "Non renseigné"),
integrity(evidence_record_get_integrity_status(evidence)),
optional(evidence_record_get_description(evidence), "Aucune"));
}
g_string_append(summary,
"\nAVERTISSEMENTS\n"
"Aucune écriture na encore été effectuée.\n"
"Les informations seront créées seulement après un clic sur "
"« Créer la personne ».\n"
"Aucune identité réelle ni relation avec un auteur nest inférée "
"automatiquement.");
g_free(size);
g_free(short_sha);
return g_string_free(summary, FALSE);
}

View file

@ -0,0 +1,35 @@
#include "core/person_creation_guard.h"
struct PersonCreationGuard {
guint64 generation;
char *root_path;
char *database_path;
};
PersonCreationGuard *person_creation_guard_new(guint64 generation,
const char *root_path, const char *database_path)
{
if (root_path == NULL || database_path == NULL) return NULL;
PersonCreationGuard *g = g_new0(PersonCreationGuard, 1);
g->generation = generation;
g->root_path = g_canonicalize_filename(root_path, NULL);
g->database_path = g_canonicalize_filename(database_path, NULL);
return g;
}
void person_creation_guard_free(PersonCreationGuard *g)
{
if (g == NULL) return;
g_free(g->root_path); g_free(g->database_path); g_free(g);
}
gboolean person_creation_guard_matches(const PersonCreationGuard *g,
gboolean open, guint64 generation, const char *root, const char *database)
{
char *canonical_root, *canonical_database;
gboolean matches;
if (g == NULL || !open || root == NULL || database == NULL ||
generation != g->generation) return FALSE;
canonical_root = g_canonicalize_filename(root, NULL);
canonical_database = g_canonicalize_filename(database, NULL);
matches = g_strcmp0(canonical_root, g->root_path) == 0 &&
g_strcmp0(canonical_database, g->database_path) == 0;
g_free(canonical_root); g_free(canonical_database);
return matches;
}

View file

@ -0,0 +1,78 @@
#include "core/person_dialog_lifecycle.h"
struct PersonDialogLifecycle
{
guint64 generation;
gboolean preview_active;
gboolean finished;
gboolean cancelled;
};
PersonDialogLifecycle *person_dialog_lifecycle_new(void)
{
return g_new0(PersonDialogLifecycle, 1);
}
void person_dialog_lifecycle_free(PersonDialogLifecycle *lifecycle)
{
g_free(lifecycle);
}
guint64 person_dialog_lifecycle_begin_preview(
PersonDialogLifecycle *lifecycle)
{
if (lifecycle == NULL || lifecycle->finished) return 0;
lifecycle->generation++;
lifecycle->preview_active = TRUE;
return lifecycle->generation;
}
gboolean person_dialog_lifecycle_cancel(PersonDialogLifecycle *lifecycle)
{
if (lifecycle == NULL || lifecycle->finished) return FALSE;
lifecycle->finished = TRUE;
lifecycle->cancelled = TRUE;
lifecycle->preview_active = FALSE;
lifecycle->generation++;
return TRUE;
}
gboolean person_dialog_lifecycle_complete(PersonDialogLifecycle *lifecycle)
{
if (lifecycle == NULL || lifecycle->finished) return FALSE;
lifecycle->finished = TRUE;
lifecycle->preview_active = FALSE;
lifecycle->generation++;
return TRUE;
}
gboolean person_dialog_lifecycle_accepts_preview(
const PersonDialogLifecycle *lifecycle, guint64 generation)
{
return lifecycle != NULL && !lifecycle->finished &&
lifecycle->preview_active && generation == lifecycle->generation;
}
gboolean person_dialog_lifecycle_is_cancelled(
const PersonDialogLifecycle *lifecycle)
{
return lifecycle != NULL && lifecycle->cancelled;
}
gboolean person_dialog_lifecycle_is_finished(
const PersonDialogLifecycle *lifecycle)
{
return lifecycle != NULL && lifecycle->finished;
}
gboolean person_dialog_lifecycle_has_preview(
const PersonDialogLifecycle *lifecycle)
{
return lifecycle != NULL && lifecycle->preview_active;
}
guint64 person_dialog_lifecycle_get_generation(
const PersonDialogLifecycle *lifecycle)
{
return lifecycle != NULL ? lifecycle->generation : 0;
}

View file

@ -3,6 +3,7 @@
* @brief Création transactionnelle d'une personne observée. * @brief Création transactionnelle d'une personne observée.
******************************************************************************/ ******************************************************************************/
#include "core/person_entity_service.h" #include "core/person_entity_service.h"
#include "dao/person_role_assignment_dao.h"
#include "dao/entity_dao.h" #include "dao/entity_dao.h"
#include "dao/evidence_entity_dao.h" #include "dao/evidence_entity_dao.h"
#include "database/transaction.h" #include "database/transaction.h"
@ -188,6 +189,7 @@ gboolean person_entity_service_create(Database *database,
{ {
EntityDao *entity_dao = NULL; EntityDao *entity_dao = NULL;
EvidenceEntityDao *link_dao = NULL; EvidenceEntityDao *link_dao = NULL;
PersonRoleAssignmentDao *role_dao = NULL;
EntityRecord *record = NULL; EntityRecord *record = NULL;
GDateTime *now = NULL; GDateTime *now = NULL;
char *timestamp = NULL; char *timestamp = NULL;
@ -208,6 +210,16 @@ gboolean person_entity_service_create(Database *database,
"Les informations de la personne sont invalides."); "Les informations de la personne sont invalides.");
return FALSE; return FALSE;
} }
for (guint i = 0; input->role_assignments != NULL &&
i < input->role_assignments->len; i++)
if (!person_role_assignment_input_is_valid(g_ptr_array_index(
(GPtrArray *) input->role_assignments, i)))
{
g_set_error_literal(error, g_quark_from_static_string(
"person-entity-service-error"), 1,
"Une affectation de rôle est invalide.");
return FALSE;
}
value = input->declared_name != NULL && input->declared_name[0] != '\0' value = input->declared_name != NULL && input->declared_name[0] != '\0'
? input->declared_name ? input->declared_name
: input->pseudonym != NULL && input->pseudonym[0] != '\0' : input->pseudonym != NULL && input->pseudonym[0] != '\0'
@ -232,6 +244,10 @@ gboolean person_entity_service_create(Database *database,
timestamp, timestamp, ENTITY_STATUS_ACTIVE, error); timestamp, timestamp, ENTITY_STATUS_ACTIVE, error);
if (entity_dao == NULL || record == NULL || if (entity_dao == NULL || record == NULL ||
!entity_dao_insert(entity_dao, record, error)) goto cleanup; !entity_dao_insert(entity_dao, record, error)) goto cleanup;
role_dao = person_role_assignment_dao_new(database, error);
if (role_dao == NULL ||
!person_role_assignment_dao_insert_all(role_dao, identifier,
input->role_assignments, error)) goto cleanup;
if (input->evidence_identifier != NULL && input->evidence_identifier[0] != '\0') if (input->evidence_identifier != NULL && input->evidence_identifier[0] != '\0')
{ {
link_dao = evidence_entity_dao_new(database, error); link_dao = evidence_entity_dao_new(database, error);
@ -243,6 +259,7 @@ gboolean person_entity_service_create(Database *database,
success = TRUE; success = TRUE;
if (out_identifier != NULL) *out_identifier = g_strdup(identifier); if (out_identifier != NULL) *out_identifier = g_strdup(identifier);
cleanup: cleanup:
person_role_assignment_dao_free(role_dao);
if (!success && active) database_transaction_rollback(database); if (!success && active) database_transaction_rollback(database);
evidence_entity_dao_free(link_dao); evidence_entity_dao_free(link_dao);
entity_record_free(record); entity_record_free(record);

View file

@ -84,7 +84,9 @@ static const char *const evidence_dao_find_by_identifier_sql =
" preuves.collected_at," " preuves.collected_at,"
" preuves.source," " preuves.source,"
" preuves.description," " preuves.description,"
" preuves.integrity_status " " preuves.integrity_status,"
" types_preuve.label,"
" preuves.mime_type "
"FROM preuves " "FROM preuves "
"LEFT JOIN types_preuve " "LEFT JOIN types_preuve "
"ON types_preuve.id = preuves.type_id " "ON types_preuve.id = preuves.type_id "
@ -140,7 +142,9 @@ static const char *const evidence_dao_list_all_sql =
" preuves.collected_at," " preuves.collected_at,"
" preuves.source," " preuves.source,"
" preuves.description," " preuves.description,"
" preuves.integrity_status " " preuves.integrity_status,"
" types_preuve.label,"
" preuves.mime_type "
"FROM preuves " "FROM preuves "
"LEFT JOIN types_preuve " "LEFT JOIN types_preuve "
"ON types_preuve.id = preuves.type_id " "ON types_preuve.id = preuves.type_id "
@ -456,6 +460,8 @@ static EvidenceRecord *evidence_dao_read_current_record(
char *collected_at = NULL; char *collected_at = NULL;
char *source = NULL; char *source = NULL;
char *description = NULL; char *description = NULL;
char *type_label = NULL;
char *mime_type = NULL;
int64_t size_bytes = -1; int64_t size_bytes = -1;
int64_t integrity_status_value = -1; int64_t integrity_status_value = -1;
@ -534,6 +540,9 @@ static EvidenceRecord *evidence_dao_read_current_record(
statement, statement,
11, 11,
&integrity_status_value &integrity_status_value
) ||
!database_statement_column_text(statement, 12, &type_label) ||
!database_statement_column_text(statement, 13, &mime_type
)) ))
{ {
evidence_dao_set_error_literal( evidence_dao_set_error_literal(
@ -608,6 +617,9 @@ static EvidenceRecord *evidence_dao_read_current_record(
); );
} }
} }
else
evidence_record_set_display_metadata(
evidence_record, type_label, mime_type);
cleanup: cleanup:
@ -618,6 +630,8 @@ cleanup:
g_free( g_free(
description description
); );
g_free(type_label);
g_free(mime_type);
g_free( g_free(
source source

View file

@ -0,0 +1,142 @@
#include "dao/person_role_assignment_dao.h"
#include "database/statement.h"
struct PersonRoleAssignmentDao { Database *database; };
static GQuark role_dao_error(void)
{ return g_quark_from_static_string("person-role-assignment-dao-error"); }
PersonRoleAssignmentDao *person_role_assignment_dao_new(
Database *database, GError **error)
{
if (database == NULL) {
g_set_error_literal(error, role_dao_error(), 1,
"La connexion SQLite est invalide.");
return NULL;
}
PersonRoleAssignmentDao *dao = g_new0(PersonRoleAssignmentDao, 1);
dao->database = database;
return dao;
}
void person_role_assignment_dao_free(PersonRoleAssignmentDao *dao)
{ g_free(dao); }
gboolean person_role_assignment_dao_insert(PersonRoleAssignmentDao *dao,
const char *entity_identifier, const PersonRoleAssignmentInput *input,
GError **error)
{
static const char sql[] =
"INSERT INTO person_role_assignments(id,entity_id,role_code,"
"evidence_id,provenance_kind,confidence,notes,created_at,updated_at) "
"SELECT ?,?,?,?,?,?,?,?,? WHERE EXISTS(SELECT 1 FROM entites e "
"JOIN types_entite t ON t.id=e.type_id WHERE e.id=? AND t.code='person') "
"AND (? IS NULL OR EXISTS(SELECT 1 FROM preuves WHERE id=?)) "
"RETURNING id;";
DatabaseStatement *statement = NULL;
GDateTime *now = NULL;
char *timestamp = NULL, *identifier = NULL;
gboolean success = FALSE;
if (dao == NULL || !g_uuid_string_is_valid(entity_identifier) ||
!person_role_assignment_input_is_valid(input)) goto invalid;
now = g_date_time_new_now_utc();
timestamp = now != NULL ? g_date_time_format(now,
"%Y-%m-%dT%H:%M:%SZ") : NULL;
identifier = g_uuid_string_random();
statement = database_statement_prepare(dao->database, sql);
if (statement == NULL || timestamp == NULL || identifier == NULL ||
!database_statement_bind_text(statement, 1, identifier) ||
!database_statement_bind_text(statement, 2, entity_identifier) ||
!database_statement_bind_text(statement, 3, input->role_code) ||
!(input->evidence_identifier != NULL
? database_statement_bind_text(statement, 4, input->evidence_identifier)
: database_statement_bind_null(statement, 4)) ||
!database_statement_bind_text(statement, 5, input->provenance_kind) ||
!(input->has_confidence
? database_statement_bind_int64(statement, 6, input->confidence)
: database_statement_bind_null(statement, 6)) ||
!(input->notes != NULL
? database_statement_bind_text(statement, 7, input->notes)
: database_statement_bind_null(statement, 7)) ||
!database_statement_bind_text(statement, 8, timestamp) ||
!database_statement_bind_text(statement, 9, timestamp) ||
!database_statement_bind_text(statement, 10, entity_identifier) ||
!(input->evidence_identifier != NULL
? database_statement_bind_text(statement, 11, input->evidence_identifier)
: database_statement_bind_null(statement, 11)) ||
!(input->evidence_identifier != NULL
? database_statement_bind_text(statement, 12, input->evidence_identifier)
: database_statement_bind_null(statement, 12)) ||
database_statement_step(statement) != DATABASE_STATEMENT_STEP_ROW)
goto failed;
success = TRUE;
goto cleanup;
invalid:
g_set_error_literal(error, role_dao_error(), 1,
"L'affectation de rôle est invalide.");
goto cleanup;
failed:
g_set_error_literal(error, role_dao_error(), 2,
"La personne, la preuve ou l'affectation de rôle est invalide.");
cleanup:
database_statement_finalize(statement);
g_free(identifier); g_free(timestamp);
g_clear_pointer(&now, g_date_time_unref);
return success;
}
gboolean person_role_assignment_dao_insert_all(PersonRoleAssignmentDao *dao,
const char *entity_identifier, const GPtrArray *inputs, GError **error)
{
for (guint i = 0; inputs != NULL && i < inputs->len; i++)
if (!person_role_assignment_dao_insert(dao, entity_identifier,
g_ptr_array_index((GPtrArray *) inputs, i), error))
return FALSE;
return TRUE;
}
GPtrArray *person_role_assignment_dao_list_by_entity(
PersonRoleAssignmentDao *dao, const char *entity_identifier, GError **error)
{
static const char sql[] = "SELECT id,role_code,evidence_id,provenance_kind,"
"confidence,notes,created_at,updated_at FROM person_role_assignments "
"WHERE entity_id=? ORDER BY created_at,id;";
DatabaseStatement *s = NULL;
GPtrArray *result = NULL;
if (dao == NULL || !g_uuid_string_is_valid(entity_identifier)) goto failed;
s = database_statement_prepare(dao->database, sql);
result = g_ptr_array_new_with_free_func(
(GDestroyNotify) person_role_assignment_record_free);
if (s == NULL || result == NULL ||
!database_statement_bind_text(s, 1, entity_identifier)) goto failed;
while (database_statement_step(s) == DATABASE_STATEMENT_STEP_ROW) {
PersonRoleAssignmentRecord *r = g_new0(PersonRoleAssignmentRecord, 1);
int64_t confidence = 0;
bool confidence_is_null = true;
if (!database_statement_column_text(s, 0, &r->identifier) ||
!database_statement_column_text(s, 1, &r->assignment.role_code) ||
!database_statement_column_text(
s, 2, &r->assignment.evidence_identifier) ||
!database_statement_column_text(
s, 3, &r->assignment.provenance_kind) ||
!database_statement_column_is_null(s, 4, &confidence_is_null) ||
!database_statement_column_text(s, 5, &r->assignment.notes) ||
!database_statement_column_text(s, 6, &r->created_at) ||
!database_statement_column_text(s, 7, &r->updated_at)) {
person_role_assignment_record_free(r);
goto failed;
}
r->entity_identifier = g_strdup(entity_identifier);
r->assignment.has_confidence = !confidence_is_null;
if (r->assignment.has_confidence &&
database_statement_column_int64(s, 4, &confidence))
r->assignment.confidence = (gint) confidence;
g_ptr_array_add(result, r);
}
database_statement_finalize(s);
return result;
failed:
database_statement_finalize(s);
g_clear_pointer(&result, g_ptr_array_unref);
g_set_error_literal(error, role_dao_error(), 2,
"Impossible de lire les affectations de rôle.");
return NULL;
}

View file

@ -16,12 +16,12 @@
/** /**
* @brief Version actuelle du schéma SQLite. * @brief Version actuelle du schéma SQLite.
*/ */
#define DATABASE_SCHEMA_VERSION_CURRENT 13 #define DATABASE_SCHEMA_VERSION_CURRENT 14
/** /**
* @brief Version actuelle sous forme textuelle pour metadata. * @brief Version actuelle sous forme textuelle pour metadata.
*/ */
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "13" #define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "14"
/** /**
* @brief Nom de l'application enregistré dans les métadonnées. * @brief Nom de l'application enregistré dans les métadonnées.
@ -857,6 +857,21 @@ rollback:
return false; return false;
} }
static bool database_migrate_v13_to_v14(Database *database)
{
bool transaction_started = false;
if (database == NULL || !database_transaction_begin(database)) return false;
transaction_started = true;
if (!schema_install_v14(database) ||
!database_update_schema_version(database, "14") ||
!database_transaction_commit(database)) goto rollback;
return true;
rollback:
if (transaction_started && !database_transaction_rollback(database))
g_warning("Impossible dannuler la migration SQLite V13 vers V14.");
return false;
}
/** /**
* @brief Garantit atomiquement la présence des extensions du schéma courant. * @brief Garantit atomiquement la présence des extensions du schéma courant.
*/ */
@ -1098,6 +1113,10 @@ bool database_migrate_to_latest(
if (!database_migrate_v12_to_v13(database)) return false; if (!database_migrate_v12_to_v13(database)) return false;
schema_version = 13; schema_version = 13;
break; break;
case 13:
if (!database_migrate_v13_to_v14(database)) return false;
schema_version = 14;
break;
default: default:
database_set_error( database_set_error(

View file

@ -371,6 +371,12 @@ bool schema_install_v13(Database *database)
"la migration SQLite V13"); "la migration SQLite V13");
} }
bool schema_install_v14(Database *database)
{
return schema_execute_file(database, "database/schema_v14.sql",
"la migration SQLite V14");
}
bool schema_ensure_current( bool schema_ensure_current(
Database *database Database *database
) )

View file

@ -18,6 +18,8 @@ struct EvidenceRecord
char *internal_name; char *internal_name;
char *relative_path; char *relative_path;
char *type_identifier; char *type_identifier;
char *type_label;
char *mime_type;
guint64 size_bytes; guint64 size_bytes;
@ -30,6 +32,28 @@ struct EvidenceRecord
EvidenceIntegrityStatus integrity_status; EvidenceIntegrityStatus integrity_status;
}; };
EvidenceRecord *evidence_record_copy(const EvidenceRecord *evidence_record)
{
EvidenceRecord *copy;
if (evidence_record == NULL) return NULL;
copy = g_new0(EvidenceRecord, 1);
copy->identifier = g_strdup(evidence_record->identifier);
copy->original_name = g_strdup(evidence_record->original_name);
copy->internal_name = g_strdup(evidence_record->internal_name);
copy->relative_path = g_strdup(evidence_record->relative_path);
copy->type_identifier = g_strdup(evidence_record->type_identifier);
copy->type_label = g_strdup(evidence_record->type_label);
copy->mime_type = g_strdup(evidence_record->mime_type);
copy->size_bytes = evidence_record->size_bytes;
copy->sha256 = g_strdup(evidence_record->sha256);
copy->imported_at = g_strdup(evidence_record->imported_at);
copy->collected_at = g_strdup(evidence_record->collected_at);
copy->source = g_strdup(evidence_record->source);
copy->description = g_strdup(evidence_record->description);
copy->integrity_status = evidence_record->integrity_status;
return copy;
}
/** /**
* @brief Produit une erreur du domaine EvidenceRecord. * @brief Produit une erreur du domaine EvidenceRecord.
*/ */
@ -800,6 +824,8 @@ void evidence_record_free(
g_free( g_free(
evidence_record->type_identifier evidence_record->type_identifier
); );
g_free(evidence_record->type_label);
g_free(evidence_record->mime_type);
g_free( g_free(
evidence_record->relative_path evidence_record->relative_path
@ -882,6 +908,28 @@ const char *evidence_record_get_type_identifier(
return evidence_record->type_identifier; return evidence_record->type_identifier;
} }
void evidence_record_set_display_metadata(EvidenceRecord *record,
const char *type_label, const char *mime_type)
{
if (record == NULL) return;
g_free(record->type_label);
g_free(record->mime_type);
record->type_label = g_strdup(type_label);
record->mime_type = g_strdup(mime_type);
}
const char *evidence_record_get_type_label(const EvidenceRecord *record)
{
if (record == NULL) return NULL;
return record->type_label != NULL
? record->type_label : record->type_identifier;
}
const char *evidence_record_get_mime_type(const EvidenceRecord *record)
{
return record != NULL ? record->mime_type : NULL;
}
guint64 evidence_record_get_size_bytes( guint64 evidence_record_get_size_bytes(
const EvidenceRecord *evidence_record const EvidenceRecord *evidence_record
) )

View file

@ -0,0 +1,138 @@
#include "models/evidence_selection_model.h"
struct EvidenceSelectionModel {
GPtrArray *records;
char *query;
char *type_code;
char *selected_identifier;
};
static gint compare_strings(gconstpointer left, gconstpointer right)
{
return g_strcmp0(*(char * const *) left, *(char * const *) right);
}
static char *fold(const char *text)
{
char *normalized, *result;
if (text == NULL) return g_strdup("");
normalized = g_utf8_normalize(text, -1, G_NORMALIZE_ALL_COMPOSE);
if (normalized == NULL) return g_strdup("");
result = g_utf8_casefold(normalized, -1);
g_free(normalized);
return result != NULL ? result : g_strdup("");
}
static gboolean matches(const EvidenceSelectionModel *m,
const EvidenceRecord *r)
{
gboolean found = FALSE;
char *needle, *fields[5];
if (m->type_code != NULL && g_strcmp0(m->type_code,
evidence_record_get_type_identifier(r)) != 0) return FALSE;
needle = fold(m->query);
if (needle[0] == '\0') found = TRUE;
fields[0] = fold(evidence_record_get_original_name(r));
fields[1] = fold(evidence_record_get_description(r));
fields[2] = fold(evidence_record_get_type_identifier(r));
fields[3] = fold(evidence_record_get_type_label(r));
fields[4] = fold(evidence_record_get_mime_type(r));
for (guint i = 0; !found && i < G_N_ELEMENTS(fields); i++)
found = strstr(fields[i], needle) != NULL;
for (guint i = 0; i < G_N_ELEMENTS(fields); i++) g_free(fields[i]);
g_free(needle);
return found;
}
EvidenceSelectionModel *evidence_selection_model_new(const GPtrArray *records)
{
EvidenceSelectionModel *m = g_new0(EvidenceSelectionModel, 1);
m->records = g_ptr_array_new_with_free_func(
(GDestroyNotify) evidence_record_free);
for (guint i = 0; records != NULL && i < records->len; i++) {
EvidenceRecord *copy = evidence_record_copy(
g_ptr_array_index((GPtrArray *) records, i));
if (copy != NULL) g_ptr_array_add(m->records, copy);
}
return m;
}
void evidence_selection_model_free(EvidenceSelectionModel *m)
{
if (m == NULL) return;
g_ptr_array_unref(m->records); g_free(m->query);
g_free(m->type_code); g_free(m->selected_identifier); g_free(m);
}
static void invalidate_hidden(EvidenceSelectionModel *m)
{
if (m->selected_identifier == NULL) return;
for (guint i = 0; i < m->records->len; i++) {
EvidenceRecord *r = g_ptr_array_index(m->records, i);
if (g_strcmp0(evidence_record_get_identifier(r),
m->selected_identifier) == 0 && matches(m, r)) return;
}
g_clear_pointer(&m->selected_identifier, g_free);
}
void evidence_selection_model_set_query(EvidenceSelectionModel *m,
const char *query)
{
if (m == NULL) return;
g_free(m->query); m->query = g_strdup(query); invalidate_hidden(m);
}
void evidence_selection_model_set_type(EvidenceSelectionModel *m,
const char *type_code)
{
if (m == NULL) return;
g_free(m->type_code);
m->type_code = type_code != NULL && type_code[0] != '\0'
? g_strdup(type_code) : NULL;
invalidate_hidden(m);
}
GPtrArray *evidence_selection_model_list_visible(const EvidenceSelectionModel *m)
{
GPtrArray *result = g_ptr_array_new();
for (guint i = 0; m != NULL && i < m->records->len; i++) {
EvidenceRecord *r = g_ptr_array_index(m->records, i);
if (matches(m, r)) g_ptr_array_add(result, r);
}
return result;
}
gboolean evidence_selection_model_select(EvidenceSelectionModel *m,
const char *identifier)
{
if (m == NULL) return FALSE;
g_clear_pointer(&m->selected_identifier, g_free);
if (identifier == NULL) return TRUE;
for (guint i = 0; i < m->records->len; i++) {
EvidenceRecord *r = g_ptr_array_index(m->records, i);
if (matches(m, r) && g_strcmp0(
evidence_record_get_identifier(r), identifier) == 0) {
m->selected_identifier = g_strdup(identifier);
return TRUE;
}
}
return FALSE;
}
const EvidenceRecord *evidence_selection_model_get_selected(
const EvidenceSelectionModel *m)
{
for (guint i = 0; m != NULL && m->selected_identifier != NULL &&
i < m->records->len; i++) {
EvidenceRecord *r = g_ptr_array_index(m->records, i);
if (g_strcmp0(evidence_record_get_identifier(r),
m->selected_identifier) == 0) return r;
}
return NULL;
}
GPtrArray *evidence_selection_model_list_type_codes(
const EvidenceSelectionModel *m)
{
GPtrArray *types = g_ptr_array_new_with_free_func(g_free);
for (guint i = 0; m != NULL && i < m->records->len; i++) {
const char *code = evidence_record_get_type_identifier(
g_ptr_array_index(m->records, i));
gboolean exists = FALSE;
for (guint j = 0; code != NULL && j < types->len; j++)
if (g_strcmp0(code, g_ptr_array_index(types, j)) == 0)
exists = TRUE;
if (code != NULL && !exists) g_ptr_array_add(types, g_strdup(code));
}
g_ptr_array_sort(types, compare_strings);
return types;
}

View file

@ -0,0 +1,89 @@
#include "models/person_role_assignment.h"
#include <string.h>
typedef struct { const char *code; const char *label; } PersonRoleDefinition;
static const PersonRoleDefinition roles[] = {
{"alleged_author", "Auteur présumé"},
{"presented_identity", "Identité présentée"},
{"potentially_impersonated_identity", "Identité potentiellement usurpée"},
{"victim", "Victime"},
{"witness", "Témoin"},
{"declared_bank_holder", "Titulaire bancaire déclaré"},
{"intermediary", "Intermédiaire"},
{"mentioned_person", "Personne citée"},
{"other", "Autre"},
/* Codes V6 conservés pour la lecture et la migration historique. */
{"uncategorized", "Non catégorisée"},
{"alleged_scammer", "Escroc présumé (historique)"},
{"suspect", "Suspect (historique)"},
{"related_person", "Personne liée (historique)"},
{"impersonated_identity", "Identité usurpée (historique)"}
};
gboolean person_role_assignment_role_is_known(const char *role_code)
{
if (role_code == NULL) return FALSE;
for (guint i = 0; i < G_N_ELEMENTS(roles); i++)
if (g_str_equal(role_code, roles[i].code)) return TRUE;
return FALSE;
}
const char *person_role_assignment_role_label(const char *role_code)
{
for (guint i = 0; role_code != NULL && i < G_N_ELEMENTS(roles); i++)
if (g_str_equal(role_code, roles[i].code)) return roles[i].label;
return NULL;
}
gboolean person_role_assignment_input_is_valid(
const PersonRoleAssignmentInput *input)
{
return input != NULL &&
person_role_assignment_role_is_known(input->role_code) &&
(input->evidence_identifier == NULL ||
g_uuid_string_is_valid(input->evidence_identifier)) &&
input->provenance_kind != NULL &&
(g_str_equal(input->provenance_kind, "manual") ||
g_str_equal(input->provenance_kind, "legacy_manual")) &&
(!input->has_confidence ||
(input->confidence >= 0 && input->confidence <= 100));
}
PersonRoleAssignmentInput *person_role_assignment_input_copy(
const PersonRoleAssignmentInput *input)
{
PersonRoleAssignmentInput *copy;
if (input == NULL) return NULL;
copy = g_new0(PersonRoleAssignmentInput, 1);
copy->role_code = g_strdup(input->role_code);
copy->evidence_identifier = g_strdup(input->evidence_identifier);
copy->provenance_kind = g_strdup(input->provenance_kind);
copy->has_confidence = input->has_confidence;
copy->confidence = input->confidence;
copy->notes = g_strdup(input->notes);
return copy;
}
void person_role_assignment_input_free(PersonRoleAssignmentInput *input)
{
if (input == NULL) return;
g_free(input->role_code);
g_free(input->evidence_identifier);
g_free(input->provenance_kind);
g_free(input->notes);
g_free(input);
}
void person_role_assignment_record_free(PersonRoleAssignmentRecord *record)
{
if (record == NULL) return;
g_free(record->identifier);
g_free(record->entity_identifier);
g_free(record->assignment.role_code);
g_free(record->assignment.evidence_identifier);
g_free(record->assignment.provenance_kind);
g_free(record->assignment.notes);
g_free(record->created_at);
g_free(record->updated_at);
g_free(record);
}

View file

@ -3,6 +3,10 @@
* @brief Formulaire GTK de création d'une personne observée. * @brief Formulaire GTK de création d'une personne observée.
******************************************************************************/ ******************************************************************************/
#include "views/create_person_dialog.h" #include "views/create_person_dialog.h"
#include "core/evidence_preview_task.h"
#include "core/person_confirmation_summary.h"
#include "core/person_dialog_lifecycle.h"
#include "models/evidence_selection_model.h"
struct CreatePersonDialogResult struct CreatePersonDialogResult
{ {
@ -13,6 +17,7 @@ struct CreatePersonDialogResult
char *status; char *status;
char *notes; char *notes;
char *evidence_identifier; char *evidence_identifier;
GPtrArray *role_assignments;
}; };
typedef struct typedef struct
{ {
@ -23,14 +28,44 @@ typedef struct
GtkDropDown *status; GtkDropDown *status;
GtkSpinButton *confidence; GtkSpinButton *confidence;
GtkDropDown *evidence; GtkDropDown *evidence;
GtkDropDown *type_filter;
GtkEntry *search;
GtkTextView *notes; GtkTextView *notes;
GtkLabel *error; GtkLabel *error;
GtkLabel *metadata;
GtkLabel *preview_status;
GtkPicture *preview_picture;
GtkLabel *progress;
GtkLabel *summary;
GtkStack *stack;
GtkButton *previous;
GtkButton *next;
GtkButton *create;
GtkCheckButton *roles[9];
guint step;
GPtrArray *evidence_identifiers; GPtrArray *evidence_identifiers;
GPtrArray *visible_records;
GPtrArray *type_codes;
EvidenceSelectionModel *selection_model;
char *investigation_root_path;
TaskManager *task_manager;
BackgroundTask *preview_task;
PersonDialogLifecycle *lifecycle;
CreatePersonDialogCallback callback; CreatePersonDialogCallback callback;
CreatePersonDialogSessionCheck session_check;
gpointer user_data; gpointer user_data;
gboolean completed; GDestroyNotify user_data_destroy;
} CreatePersonDialogState; } CreatePersonDialogState;
typedef struct {
GWeakRef window;
char *evidence_identifier;
guint64 generation;
} CreatePersonPreviewContext;
static const char *const status_codes[] = {"unknown", "suspected", "confirmed"}; static const char *const status_codes[] = {"unknown", "suspected", "confirmed"};
static void create_person_dialog_clear_preview(CreatePersonDialogState *state);
static void create_person_dialog_select_record(CreatePersonDialogState *state,
const EvidenceRecord *record);
/** @brief Copie et nettoie un texte facultatif. */ /** @brief Copie et nettoie un texte facultatif. */
static char *create_person_dialog_copy(const char *text) static char *create_person_dialog_copy(const char *text)
@ -54,16 +89,230 @@ static void create_person_dialog_state_free(gpointer data)
{ {
CreatePersonDialogState *state = data; CreatePersonDialogState *state = data;
if (state == NULL) return; if (state == NULL) return;
if (state->preview_task != NULL) {
background_task_cancel(state->preview_task);
background_task_unref(state->preview_task);
}
person_dialog_lifecycle_cancel(state->lifecycle);
g_clear_pointer(&state->evidence_identifiers, g_ptr_array_unref); g_clear_pointer(&state->evidence_identifiers, g_ptr_array_unref);
g_clear_pointer(&state->visible_records, g_ptr_array_unref);
g_clear_pointer(&state->type_codes, g_ptr_array_unref);
g_clear_pointer(&state->selection_model, evidence_selection_model_free);
g_clear_pointer(&state->lifecycle, person_dialog_lifecycle_free);
g_free(state->investigation_root_path);
if (state->user_data_destroy != NULL)
state->user_data_destroy(state->user_data);
g_free(state); g_free(state);
} }
static void create_person_dialog_rebuild_evidence(CreatePersonDialogState *state)
{
GtkStringList *labels = gtk_string_list_new(NULL);
const EvidenceRecord *selected =
evidence_selection_model_get_selected(state->selection_model);
const char *selected_id = selected != NULL
? evidence_record_get_identifier(selected) : NULL;
guint selected_index = 0;
gtk_string_list_append(labels, "Aucune preuve associée");
g_clear_pointer(&state->visible_records, g_ptr_array_unref);
state->visible_records =
evidence_selection_model_list_visible(state->selection_model);
for (guint i = 0; i < state->visible_records->len; i++) {
EvidenceRecord *record = g_ptr_array_index(state->visible_records, i);
gtk_string_list_append(labels,
evidence_record_get_original_name(record));
if (g_strcmp0(selected_id,
evidence_record_get_identifier(record)) == 0)
selected_index = i + 1;
}
gtk_drop_down_set_model(state->evidence, G_LIST_MODEL(labels));
gtk_drop_down_set_selected(state->evidence, selected_index);
g_object_unref(labels);
if (selected_index == 0) {
evidence_selection_model_select(state->selection_model, NULL);
create_person_dialog_clear_preview(state);
}
}
static void create_person_dialog_on_search_changed(
GtkEditable *editable, gpointer data)
{
CreatePersonDialogState *state = data;
evidence_selection_model_set_query(state->selection_model,
gtk_editable_get_text(editable));
create_person_dialog_rebuild_evidence(state);
}
static void create_person_dialog_on_type_changed(
GtkDropDown *dropdown, GParamSpec *pspec, gpointer data)
{
CreatePersonDialogState *state = data;
guint selected = gtk_drop_down_get_selected(dropdown);
(void) pspec;
evidence_selection_model_set_type(state->selection_model,
selected > 0 && selected - 1 < state->type_codes->len
? g_ptr_array_index(state->type_codes, selected - 1) : NULL);
create_person_dialog_rebuild_evidence(state);
}
static void create_person_dialog_on_evidence_changed(
GtkDropDown *dropdown, GParamSpec *pspec, gpointer data)
{
CreatePersonDialogState *state = data;
guint selected = gtk_drop_down_get_selected(dropdown);
(void) pspec;
create_person_dialog_select_record(state,
selected > 0 && selected - 1 < state->visible_records->len
? g_ptr_array_index(state->visible_records, selected - 1) : NULL);
}
/** @brief Termine le dialogue comme une annulation. */ /** @brief Termine le dialogue comme une annulation. */
static void create_person_dialog_cancel(CreatePersonDialogState *state) static void create_person_dialog_cancel(CreatePersonDialogState *state)
{ {
if (state == NULL || state->completed) return; if (state == NULL ||
state->completed = TRUE; !person_dialog_lifecycle_cancel(state->lifecycle)) return;
if (state->preview_task != NULL) background_task_cancel(state->preview_task);
if (state->callback != NULL) state->callback(NULL, state->user_data); if (state->callback != NULL) state->callback(NULL, state->user_data);
} }
static void preview_context_free(gpointer data)
{
CreatePersonPreviewContext *context = data;
if (context == NULL) return;
g_weak_ref_clear(&context->window);
g_free(context->evidence_identifier);
g_free(context);
}
static const char *integrity_label(EvidenceIntegrityStatus status)
{
switch (status) {
case EVIDENCE_INTEGRITY_STATUS_VALID: return "Valide";
case EVIDENCE_INTEGRITY_STATUS_MISSING: return "Fichier absent";
case EVIDENCE_INTEGRITY_STATUS_MODIFIED: return "Invalide";
case EVIDENCE_INTEGRITY_STATUS_ERROR: return "Erreur de vérification";
default: return "Non vérifiée";
}
}
static void create_person_dialog_clear_preview(CreatePersonDialogState *state)
{
if (state->preview_task != NULL) {
background_task_cancel(state->preview_task);
background_task_unref(state->preview_task);
state->preview_task = NULL;
}
person_dialog_lifecycle_begin_preview(state->lifecycle);
gtk_picture_set_paintable(state->preview_picture, NULL);
gtk_label_set_text(state->preview_status, "Aucune preuve sélectionnée.");
gtk_label_set_text(state->metadata, "");
}
static void create_person_dialog_preview_completed(
BackgroundTask *task, gpointer data)
{
CreatePersonPreviewContext *context = data;
GtkWindow *window = g_weak_ref_get(&context->window);
CreatePersonDialogState *state = window != NULL
? g_object_get_data(G_OBJECT(window), "person-dialog-state") : NULL;
EvidencePreviewResult *result = background_task_get_result(task);
if (state != NULL && state->session_check != NULL &&
!state->session_check(state->user_data)) {
person_dialog_lifecycle_cancel(state->lifecycle);
gtk_label_set_text(state->preview_status,
"Aperçu annulé : l'enquête active a changé.");
gtk_widget_set_sensitive(GTK_WIDGET(state->create), FALSE);
} else if (state != NULL &&
person_dialog_lifecycle_accepts_preview(
state->lifecycle, context->generation) &&
evidence_preview_result_matches(result, context->evidence_identifier,
context->generation)) {
GError *error = NULL;
GdkTexture *texture = gdk_texture_new_from_bytes(
result->png_bytes, &error);
if (texture != NULL) {
gtk_picture_set_paintable(state->preview_picture,
GDK_PAINTABLE(texture));
gtk_label_set_text(state->preview_status, "Aperçu intègre.");
g_object_unref(texture);
} else {
gtk_label_set_text(state->preview_status,
error != NULL ? error->message : "Aperçu illisible.");
}
g_clear_error(&error);
} else if (state != NULL &&
person_dialog_lifecycle_accepts_preview(
state->lifecycle, context->generation)) {
GError *error = background_task_dup_error(task);
gtk_label_set_text(state->preview_status,
error != NULL ? error->message : "Aperçu annulé.");
g_clear_error(&error);
}
if (state != NULL && state->preview_task == task) {
background_task_unref(state->preview_task);
state->preview_task = NULL;
}
g_clear_object(&window);
}
static void create_person_dialog_select_record(CreatePersonDialogState *state,
const EvidenceRecord *record)
{
char *size = NULL, *sha = NULL, *text = NULL;
const char *mime;
CreatePersonPreviewContext *context;
EvidencePreviewRequest *request;
GError *error = NULL;
create_person_dialog_clear_preview(state);
if (record == NULL) return;
evidence_selection_model_select(state->selection_model,
evidence_record_get_identifier(record));
size = g_format_size(evidence_record_get_size_bytes(record));
sha = g_strndup(evidence_record_get_sha256(record), 12);
mime = evidence_record_get_mime_type(record);
text = g_strdup_printf(
"Nom : %s\nType : %s (%s)\nMIME : %s\nTaille : %s\n"
"Import : %s\nSHA-256 : %s…\nIntégrité : %s\nDescription : %s",
evidence_record_get_original_name(record),
evidence_record_get_type_label(record),
evidence_record_get_type_identifier(record),
mime != NULL ? mime : "Non renseigné", size,
evidence_record_get_imported_at(record), sha,
integrity_label(evidence_record_get_integrity_status(record)),
evidence_record_get_description(record) != NULL
? evidence_record_get_description(record) : "Aucune");
gtk_label_set_text(state->metadata, text);
if (mime == NULL || !(g_str_equal(mime, "image/png") ||
g_str_equal(mime, "image/jpeg"))) {
gtk_label_set_text(state->preview_status,
"Aperçu indisponible pour ce format.");
goto cleanup;
}
gtk_label_set_text(state->preview_status,
"Vérification et chargement en cours…");
request = evidence_preview_request_new(state->investigation_root_path,
evidence_record_get_identifier(record),
evidence_record_get_relative_path(record),
evidence_record_get_sha256(record), mime,
person_dialog_lifecycle_get_generation(state->lifecycle));
context = g_new0(CreatePersonPreviewContext, 1);
g_weak_ref_init(&context->window, G_OBJECT(state->window));
context->evidence_identifier =
g_strdup(evidence_record_get_identifier(record));
context->generation =
person_dialog_lifecycle_get_generation(state->lifecycle);
state->preview_task = evidence_preview_task_start(state->task_manager,
request, create_person_dialog_preview_completed, context,
preview_context_free, &error);
evidence_preview_request_free(request);
if (state->preview_task == NULL) {
preview_context_free(context);
gtk_label_set_text(state->preview_status,
error != NULL ? error->message : "Aperçu impossible.");
}
g_clear_error(&error);
cleanup:
g_free(size); g_free(sha); g_free(text);
}
/** @brief Traite la fermeture native. */ /** @brief Traite la fermeture native. */
static gboolean create_person_dialog_on_close(GtkWindow *window, gpointer data) static gboolean create_person_dialog_on_close(GtkWindow *window, gpointer data)
{ {
@ -82,8 +331,16 @@ static void create_person_dialog_on_create(GtkButton *button, gpointer data)
CreatePersonDialogResult *result = NULL; CreatePersonDialogResult *result = NULL;
char *notes = NULL; char *notes = NULL;
guint status = gtk_drop_down_get_selected(state->status); guint status = gtk_drop_down_get_selected(state->status);
guint evidence = gtk_drop_down_get_selected(state->evidence); const EvidenceRecord *selected_record =
evidence_selection_model_get_selected(state->selection_model);
(void) button; (void) button;
if (state->session_check != NULL &&
!state->session_check(state->user_data)) {
gtk_label_set_text(state->error,
"L'enquête active a changé : création refusée.");
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
return;
}
if (status >= G_N_ELEMENTS(status_codes) || if (status >= G_N_ELEMENTS(status_codes) ||
gtk_editable_get_text(GTK_EDITABLE(state->designation))[0] == '\0') gtk_editable_get_text(GTK_EDITABLE(state->designation))[0] == '\0')
{ {
@ -100,9 +357,9 @@ static void create_person_dialog_on_create(GtkButton *button, gpointer data)
gtk_editable_get_text(GTK_EDITABLE(state->pseudonym))); gtk_editable_get_text(GTK_EDITABLE(state->pseudonym)));
result->status = g_strdup(status_codes[status]); result->status = g_strdup(status_codes[status]);
result->notes = create_person_dialog_copy(notes); result->notes = create_person_dialog_copy(notes);
if (evidence > 0 && evidence - 1 < state->evidence_identifiers->len) if (selected_record != NULL)
result->evidence_identifier = g_strdup(g_ptr_array_index( result->evidence_identifier = g_strdup(
state->evidence_identifiers, evidence - 1)); evidence_record_get_identifier(selected_record));
result->input.designation = result->designation; result->input.designation = result->designation;
result->input.declared_name = result->declared_name; result->input.declared_name = result->declared_name;
result->input.pseudonym = result->pseudonym; result->input.pseudonym = result->pseudonym;
@ -110,11 +367,103 @@ static void create_person_dialog_on_create(GtkButton *button, gpointer data)
result->input.notes = result->notes; result->input.notes = result->notes;
result->input.confidence = gtk_spin_button_get_value_as_int(state->confidence); result->input.confidence = gtk_spin_button_get_value_as_int(state->confidence);
result->input.evidence_identifier = result->evidence_identifier; result->input.evidence_identifier = result->evidence_identifier;
state->completed = TRUE; result->role_assignments = g_ptr_array_new_with_free_func(
(GDestroyNotify) person_role_assignment_input_free);
static const char *const role_codes[] = {
"alleged_author", "presented_identity",
"potentially_impersonated_identity", "victim", "witness",
"declared_bank_holder", "intermediary", "mentioned_person", "other"};
for (guint i = 0; i < G_N_ELEMENTS(role_codes); i++)
if (gtk_check_button_get_active(state->roles[i])) {
PersonRoleAssignmentInput assignment = {
.role_code = (char *) role_codes[i],
.evidence_identifier = result->evidence_identifier,
.provenance_kind = "manual",
.has_confidence = FALSE
};
g_ptr_array_add(result->role_assignments,
person_role_assignment_input_copy(&assignment));
}
result->input.role_assignments = result->role_assignments;
if (!person_dialog_lifecycle_complete(state->lifecycle)) {
create_person_dialog_result_free(result);
g_free(notes);
return;
}
if (state->callback != NULL) state->callback(result, state->user_data); if (state->callback != NULL) state->callback(result, state->user_data);
else create_person_dialog_result_free(result); else create_person_dialog_result_free(result);
g_free(notes); gtk_window_close(state->window); g_free(notes); gtk_window_close(state->window);
} }
static void create_person_dialog_update_navigation(CreatePersonDialogState *state)
{
static const char *const pages[] = {"person", "roles", "evidence", "summary"};
static const char *const steps[] = {
"1 Personne", "2 Rôles", "3 Preuve", "4 Confirmation"};
GString *progress = g_string_new(NULL);
gtk_stack_set_visible_child_name(state->stack, pages[state->step]);
for (guint i = 0; i < G_N_ELEMENTS(steps); i++) {
char *escaped = g_markup_escape_text(steps[i], -1);
if (i > 0) g_string_append(progress, "");
if (i == state->step)
g_string_append_printf(progress, "<b>%s</b>", escaped);
else
g_string_append(progress, escaped);
g_free(escaped);
}
gtk_label_set_markup(state->progress, progress->str);
g_string_free(progress, TRUE);
if (state->step == 3) {
static const char *const status_labels[] = {
"Inconnu", "Présumé", "Confirmé"};
GPtrArray *role_labels = g_ptr_array_new();
const EvidenceRecord *evidence =
evidence_selection_model_get_selected(state->selection_model);
char *notes = create_person_dialog_get_notes(state);
char *text;
for (guint i = 0; i < G_N_ELEMENTS(state->roles); i++)
if (gtk_check_button_get_active(state->roles[i]))
g_ptr_array_add(role_labels, (gpointer)
gtk_check_button_get_label(state->roles[i]));
text = person_confirmation_summary_build(
gtk_editable_get_text(GTK_EDITABLE(state->designation)),
gtk_editable_get_text(GTK_EDITABLE(state->name)),
gtk_editable_get_text(GTK_EDITABLE(state->pseudonym)),
gtk_drop_down_get_selected(state->status) <
G_N_ELEMENTS(status_labels)
? status_labels[gtk_drop_down_get_selected(state->status)]
: "Non renseigné",
gtk_spin_button_get_value_as_int(state->confidence), notes,
role_labels, evidence);
gtk_label_set_text(state->summary, text);
g_free(text);
g_free(notes);
g_ptr_array_unref(role_labels);
}
gtk_widget_set_sensitive(GTK_WIDGET(state->previous), state->step > 0);
gtk_widget_set_visible(GTK_WIDGET(state->next), state->step < 3);
gtk_widget_set_visible(GTK_WIDGET(state->create), state->step == 3);
}
static void create_person_dialog_on_previous(GtkButton *button, gpointer data)
{
CreatePersonDialogState *state = data; (void) button;
if (state->step > 0) state->step--;
create_person_dialog_update_navigation(state);
}
static void create_person_dialog_on_next(GtkButton *button, gpointer data)
{
CreatePersonDialogState *state = data; (void) button;
if (state->step == 0 &&
gtk_editable_get_text(GTK_EDITABLE(state->designation))[0] == '\0') {
gtk_label_set_text(state->error,
"La désignation de la personne est obligatoire.");
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
return;
}
gtk_widget_set_visible(GTK_WIDGET(state->error), FALSE);
if (state->step < 3) state->step++;
create_person_dialog_update_navigation(state);
}
/** @brief Ajoute une ligne libellée au formulaire. */ /** @brief Ajoute une ligne libellée au formulaire. */
static void create_person_dialog_add_row(GtkGrid *grid, int row, static void create_person_dialog_add_row(GtkGrid *grid, int row,
const char *label, GtkWidget *widget) const char *label, GtkWidget *widget)
@ -126,17 +475,33 @@ static void create_person_dialog_add_row(GtkGrid *grid, int row,
gtk_grid_attach(grid, widget, 1, row, 1, 1); gtk_grid_attach(grid, widget, 1, row, 1, 1);
} }
gboolean create_person_dialog_present(GtkWindow *parent, gboolean create_person_dialog_present(GtkWindow *parent,
const GPtrArray *records, CreatePersonDialogCallback callback, gpointer data) const GPtrArray *records, const char *investigation_root_path,
TaskManager *task_manager, CreatePersonDialogSessionCheck session_check,
CreatePersonDialogCallback callback, gpointer data,
GDestroyNotify data_destroy)
{ {
static const char *const statuses[] = {"Inconnu", "Présumé", "Confirmé", NULL}; static const char *const statuses[] = {"Inconnu", "Présumé", "Confirmé", NULL};
CreatePersonDialogState *state = NULL; CreatePersonDialogState *state = NULL;
GtkWidget *box = NULL, *grid = NULL, *actions = NULL, *cancel = NULL, *create = NULL; GtkWidget *box = NULL, *grid = NULL, *actions = NULL, *cancel = NULL;
GtkStringList *labels = NULL; GtkWidget *roles_box = NULL, *evidence_box = NULL, *summary = NULL;
if (parent == NULL) return FALSE; GtkStringList *labels = NULL, *type_labels = NULL;
if (parent == NULL || investigation_root_path == NULL ||
task_manager == NULL) return FALSE;
state = g_new0(CreatePersonDialogState, 1); state = g_new0(CreatePersonDialogState, 1);
state->callback = callback; state->user_data = data; state->callback = callback; state->session_check = session_check;
state->user_data = data; state->user_data_destroy = data_destroy;
state->evidence_identifiers = g_ptr_array_new_with_free_func(g_free); state->evidence_identifiers = g_ptr_array_new_with_free_func(g_free);
state->selection_model = evidence_selection_model_new(records);
state->visible_records =
evidence_selection_model_list_visible(state->selection_model);
state->type_codes =
evidence_selection_model_list_type_codes(state->selection_model);
state->investigation_root_path = g_strdup(investigation_root_path);
state->task_manager = task_manager;
state->lifecycle = person_dialog_lifecycle_new();
state->window = GTK_WINDOW(gtk_window_new()); state->window = GTK_WINDOW(gtk_window_new());
gtk_window_set_application(state->window,
gtk_window_get_application(parent));
gtk_window_set_title(state->window, "Ajouter une personne"); gtk_window_set_title(state->window, "Ajouter une personne");
gtk_window_set_transient_for(state->window, parent); gtk_window_set_transient_for(state->window, parent);
gtk_window_set_modal(state->window, TRUE); gtk_window_set_modal(state->window, TRUE);
@ -154,10 +519,11 @@ gboolean create_person_dialog_present(GtkWindow *parent,
gtk_drop_down_set_selected(state->status, 1); gtk_drop_down_set_selected(state->status, 1);
state->confidence = GTK_SPIN_BUTTON(gtk_spin_button_new_with_range(0, 100, 5)); state->confidence = GTK_SPIN_BUTTON(gtk_spin_button_new_with_range(0, 100, 5));
gtk_spin_button_set_value(state->confidence, 30); gtk_spin_button_set_value(state->confidence, 30);
labels = gtk_string_list_new(NULL); gtk_string_list_append(labels, "Aucune preuve associée"); labels = gtk_string_list_new(NULL);
for (guint i = 0; records != NULL && i < records->len; i++) gtk_string_list_append(labels, "Aucune preuve associée");
for (guint i = 0; i < state->visible_records->len; i++)
{ {
EvidenceRecord *record = g_ptr_array_index((GPtrArray *) records, i); EvidenceRecord *record = g_ptr_array_index(state->visible_records, i);
const char *id = evidence_record_get_identifier(record); const char *id = evidence_record_get_identifier(record);
const char *name = evidence_record_get_original_name(record); const char *name = evidence_record_get_original_name(record);
if (id == NULL || name == NULL) continue; if (id == NULL || name == NULL) continue;
@ -166,6 +532,22 @@ gboolean create_person_dialog_present(GtkWindow *parent,
} }
state->evidence = GTK_DROP_DOWN(gtk_drop_down_new(G_LIST_MODEL(labels), NULL)); state->evidence = GTK_DROP_DOWN(gtk_drop_down_new(G_LIST_MODEL(labels), NULL));
labels = NULL; labels = NULL;
state->search = GTK_ENTRY(gtk_entry_new());
gtk_entry_set_placeholder_text(state->search,
"Rechercher par nom, description ou type");
type_labels = gtk_string_list_new(NULL);
gtk_string_list_append(type_labels, "Tous les types");
for (guint i = 0; i < state->type_codes->len; i++)
gtk_string_list_append(type_labels,
g_ptr_array_index(state->type_codes, i));
state->type_filter = GTK_DROP_DOWN(
gtk_drop_down_new(G_LIST_MODEL(type_labels), NULL));
/*
* gtk_drop_down_new() prend la propriété complète du modèle. Une
* libération ici laisserait les vues internes du GtkDropDown avec un
* GListModel détruit et ferait échouer leur nettoyage à la fermeture.
*/
type_labels = NULL;
state->notes = GTK_TEXT_VIEW(gtk_text_view_new()); state->notes = GTK_TEXT_VIEW(gtk_text_view_new());
gtk_text_view_set_wrap_mode(state->notes, GTK_WRAP_WORD_CHAR); gtk_text_view_set_wrap_mode(state->notes, GTK_WRAP_WORD_CHAR);
gtk_widget_set_size_request(GTK_WIDGET(state->notes), -1, 90); gtk_widget_set_size_request(GTK_WIDGET(state->notes), -1, 90);
@ -174,18 +556,80 @@ gboolean create_person_dialog_present(GtkWindow *parent,
create_person_dialog_add_row(GTK_GRID(grid), 2, "Pseudonyme", GTK_WIDGET(state->pseudonym)); create_person_dialog_add_row(GTK_GRID(grid), 2, "Pseudonyme", GTK_WIDGET(state->pseudonym));
create_person_dialog_add_row(GTK_GRID(grid), 3, "Identification", GTK_WIDGET(state->status)); create_person_dialog_add_row(GTK_GRID(grid), 3, "Identification", GTK_WIDGET(state->status));
create_person_dialog_add_row(GTK_GRID(grid), 4, "Confiance (%)", GTK_WIDGET(state->confidence)); create_person_dialog_add_row(GTK_GRID(grid), 4, "Confiance (%)", GTK_WIDGET(state->confidence));
create_person_dialog_add_row(GTK_GRID(grid), 5, "Preuve associée", GTK_WIDGET(state->evidence)); create_person_dialog_add_row(GTK_GRID(grid), 5, "Notes factuelles", GTK_WIDGET(state->notes));
create_person_dialog_add_row(GTK_GRID(grid), 6, "Notes factuelles", GTK_WIDGET(state->notes)); state->stack = GTK_STACK(gtk_stack_new());
gtk_stack_set_transition_type(state->stack,
GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT);
gtk_stack_add_titled(state->stack, grid, "person", "1 — Personne");
roles_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
static const char *const role_codes[] = {
"alleged_author", "presented_identity",
"potentially_impersonated_identity", "victim", "witness",
"declared_bank_holder", "intermediary", "mentioned_person", "other"};
for (guint i = 0; i < G_N_ELEMENTS(role_codes); i++) {
state->roles[i] = GTK_CHECK_BUTTON(gtk_check_button_new_with_label(
person_role_assignment_role_label(role_codes[i])));
gtk_box_append(GTK_BOX(roles_box), GTK_WIDGET(state->roles[i]));
}
gtk_stack_add_titled(state->stack, roles_box, "roles", "2 — Rôles");
evidence_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
gtk_box_append(GTK_BOX(evidence_box), gtk_label_new(
"Sélectionnez une preuve existante (aucun import dans cette version)."));
gtk_box_append(GTK_BOX(evidence_box), GTK_WIDGET(state->search));
gtk_box_append(GTK_BOX(evidence_box), GTK_WIDGET(state->type_filter));
gtk_box_append(GTK_BOX(evidence_box), GTK_WIDGET(state->evidence));
state->metadata = GTK_LABEL(gtk_label_new(""));
gtk_label_set_xalign(state->metadata, 0.0f);
gtk_label_set_selectable(state->metadata, TRUE);
state->preview_status = GTK_LABEL(gtk_label_new(
"Aucune preuve sélectionnée."));
state->preview_picture = GTK_PICTURE(gtk_picture_new());
gtk_picture_set_can_shrink(state->preview_picture, TRUE);
gtk_picture_set_content_fit(state->preview_picture,
GTK_CONTENT_FIT_CONTAIN);
gtk_widget_set_size_request(GTK_WIDGET(state->preview_picture), 480, 260);
gtk_box_append(GTK_BOX(evidence_box), GTK_WIDGET(state->metadata));
gtk_box_append(GTK_BOX(evidence_box), GTK_WIDGET(state->preview_status));
gtk_box_append(GTK_BOX(evidence_box), GTK_WIDGET(state->preview_picture));
gtk_stack_add_titled(state->stack, evidence_box, "evidence", "3 — Preuve");
state->summary = GTK_LABEL(gtk_label_new(""));
summary = GTK_WIDGET(state->summary);
gtk_label_set_wrap(state->summary, TRUE);
gtk_label_set_xalign(state->summary, 0.0f);
gtk_label_set_selectable(state->summary, TRUE);
gtk_stack_add_titled(state->stack, summary, "summary", "4 — Confirmation");
state->error = GTK_LABEL(gtk_label_new(NULL)); gtk_widget_set_visible(GTK_WIDGET(state->error), FALSE); state->error = GTK_LABEL(gtk_label_new(NULL)); gtk_widget_set_visible(GTK_WIDGET(state->error), FALSE);
actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); gtk_widget_set_halign(actions, GTK_ALIGN_END); actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); gtk_widget_set_halign(actions, GTK_ALIGN_END);
cancel = gtk_button_new_with_label("Annuler"); create = gtk_button_new_with_label("Ajouter au graphe"); cancel = gtk_button_new_with_label("Annuler");
gtk_widget_add_css_class(create, "suggested-action"); state->previous = GTK_BUTTON(gtk_button_new_with_label("Précédent"));
gtk_box_append(GTK_BOX(actions), cancel); gtk_box_append(GTK_BOX(actions), create); state->next = GTK_BUTTON(gtk_button_new_with_label("Suivant"));
gtk_box_append(GTK_BOX(box), grid); gtk_box_append(GTK_BOX(box), GTK_WIDGET(state->error)); state->create = GTK_BUTTON(gtk_button_new_with_label("Créer la personne"));
gtk_widget_add_css_class(GTK_WIDGET(state->create), "suggested-action");
gtk_box_append(GTK_BOX(actions), cancel);
gtk_box_append(GTK_BOX(actions), GTK_WIDGET(state->previous));
gtk_box_append(GTK_BOX(actions), GTK_WIDGET(state->next));
gtk_box_append(GTK_BOX(actions), GTK_WIDGET(state->create));
state->progress = GTK_LABEL(gtk_label_new(NULL));
gtk_label_set_xalign(state->progress, 0.0f);
gtk_box_append(GTK_BOX(box), GTK_WIDGET(state->progress));
gtk_box_append(GTK_BOX(box), GTK_WIDGET(state->stack));
gtk_box_append(GTK_BOX(box), GTK_WIDGET(state->error));
gtk_box_append(GTK_BOX(box), actions); gtk_window_set_child(state->window, box); gtk_box_append(GTK_BOX(box), actions); gtk_window_set_child(state->window, box);
g_signal_connect(state->window, "close-request", G_CALLBACK(create_person_dialog_on_close), state); g_signal_connect(state->window, "close-request", G_CALLBACK(create_person_dialog_on_close), state);
g_signal_connect(cancel, "clicked", G_CALLBACK(create_person_dialog_on_cancel), state); g_signal_connect(cancel, "clicked", G_CALLBACK(create_person_dialog_on_cancel), state);
g_signal_connect(create, "clicked", G_CALLBACK(create_person_dialog_on_create), state); g_signal_connect(state->previous, "clicked",
G_CALLBACK(create_person_dialog_on_previous), state);
g_signal_connect(state->next, "clicked",
G_CALLBACK(create_person_dialog_on_next), state);
g_signal_connect(state->create, "clicked",
G_CALLBACK(create_person_dialog_on_create), state);
g_signal_connect(state->search, "changed",
G_CALLBACK(create_person_dialog_on_search_changed), state);
g_signal_connect(state->type_filter, "notify::selected",
G_CALLBACK(create_person_dialog_on_type_changed), state);
g_signal_connect(state->evidence, "notify::selected",
G_CALLBACK(create_person_dialog_on_evidence_changed), state);
create_person_dialog_update_navigation(state);
g_object_set_data_full(G_OBJECT(state->window), "person-dialog-state", state, g_object_set_data_full(G_OBJECT(state->window), "person-dialog-state", state,
create_person_dialog_state_free); create_person_dialog_state_free);
gtk_window_present(state->window); return TRUE; gtk_window_present(state->window); return TRUE;
@ -195,7 +639,9 @@ void create_person_dialog_result_free(CreatePersonDialogResult *result)
if (result == NULL) return; if (result == NULL) return;
g_free(result->designation); g_free(result->declared_name); g_free(result->designation); g_free(result->declared_name);
g_free(result->pseudonym); g_free(result->status); g_free(result->notes); g_free(result->pseudonym); g_free(result->status); g_free(result->notes);
g_free(result->evidence_identifier); g_free(result); g_free(result->evidence_identifier);
g_clear_pointer(&result->role_assignments, g_ptr_array_unref);
g_free(result);
} }
const PersonEntityInput *create_person_dialog_result_get_input( const PersonEntityInput *create_person_dialog_result_get_input(
const CreatePersonDialogResult *result) const CreatePersonDialogResult *result)

View file

@ -0,0 +1,157 @@
#include "views/create_person_dialog.h"
#include <gtk/gtk.h>
#include <string.h>
typedef struct
{
GtkApplication *application;
GtkWindow *main_window;
TaskManager *task_manager;
guint completion_count;
gboolean passed;
} TestContext;
static GtkWidget *find_button(GtkWidget *widget, const char *label)
{
if (GTK_IS_BUTTON(widget) &&
g_strcmp0(gtk_button_get_label(GTK_BUTTON(widget)), label) == 0)
return widget;
for (GtkWidget *child = gtk_widget_get_first_child(widget);
child != NULL; child = gtk_widget_get_next_sibling(child)) {
GtkWidget *match = find_button(child, label);
if (match != NULL) return match;
}
return NULL;
}
static GtkWidget *find_entry(GtkWidget *widget, const char *placeholder)
{
if (GTK_IS_ENTRY(widget) && g_strcmp0(
gtk_entry_get_placeholder_text(GTK_ENTRY(widget)),
placeholder) == 0)
return widget;
for (GtkWidget *child = gtk_widget_get_first_child(widget);
child != NULL; child = gtk_widget_get_next_sibling(child)) {
GtkWidget *match = find_entry(child, placeholder);
if (match != NULL) return match;
}
return NULL;
}
static GtkWidget *find_label_containing(GtkWidget *widget, const char *text)
{
if (GTK_IS_LABEL(widget) &&
strstr(gtk_label_get_text(GTK_LABEL(widget)), text) != NULL)
return widget;
for (GtkWidget *child = gtk_widget_get_first_child(widget);
child != NULL; child = gtk_widget_get_next_sibling(child)) {
GtkWidget *match = find_label_containing(child, text);
if (match != NULL) return match;
}
return NULL;
}
static void completed(CreatePersonDialogResult *result, gpointer data)
{
TestContext *context = data;
g_assert_null(result);
context->completion_count++;
}
static gboolean check_after_cancel(gpointer data)
{
TestContext *context = data;
GList *windows = gtk_application_get_windows(context->application);
g_assert_cmpuint(context->completion_count, ==, 1);
g_assert_true(gtk_widget_get_visible(GTK_WIDGET(context->main_window)));
g_assert_cmpuint(g_list_length(windows), ==, 1);
g_assert_true(windows->data == context->main_window);
context->passed = TRUE;
g_application_quit(G_APPLICATION(context->application));
return G_SOURCE_REMOVE;
}
static gboolean click_cancel(gpointer data)
{
TestContext *context = data;
GList *windows = gtk_application_get_windows(context->application);
GtkWindow *dialog = windows != NULL && windows->data != context->main_window
? windows->data : windows != NULL ? windows->next->data : NULL;
GtkWidget *button, *designation, *search;
g_assert_nonnull(dialog);
designation = find_entry(GTK_WIDGET(dialog),
"Personne présumée liée aux comptes");
g_assert_nonnull(designation);
gtk_editable_set_text(GTK_EDITABLE(designation), "SPECIMEN");
button = find_button(GTK_WIDGET(dialog), "Suivant");
g_assert_nonnull(button);
g_signal_emit_by_name(button, "clicked");
g_signal_emit_by_name(button, "clicked");
search = find_entry(GTK_WIDGET(dialog),
"Rechercher par nom, description ou type");
g_assert_nonnull(search);
gtk_editable_set_text(GTK_EDITABLE(search), "SPECIMEN");
button = find_button(GTK_WIDGET(dialog), "Suivant");
g_signal_emit_by_name(button, "clicked");
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
"Désignation : SPECIMEN"));
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
"SPECIMEN-recherche.png"));
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
"Aucune écriture na encore été effectuée"));
button = find_button(GTK_WIDGET(dialog), "Annuler");
g_assert_nonnull(button);
g_signal_emit_by_name(button, "clicked");
g_idle_add(check_after_cancel, context);
return G_SOURCE_REMOVE;
}
static void activate(GtkApplication *application, gpointer data)
{
TestContext *context = data;
GError *error = NULL;
GPtrArray *records = g_ptr_array_new_with_free_func(
(GDestroyNotify) evidence_record_free);
EvidenceRecord *record = evidence_record_new(
"10000000-0000-4000-8000-000000000001",
"SPECIMEN-recherche.png", "specimen.png",
"01_Preuves_Originales/specimen.png", "screenshot", 12,
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"2026-07-28T10:00:00Z", NULL, NULL,
"Description synthétique", EVIDENCE_INTEGRITY_STATUS_UNKNOWN,
&error);
g_assert_no_error(error);
evidence_record_set_display_metadata(
record, "Capture décran", NULL);
g_ptr_array_add(records, record);
context->main_window = GTK_WINDOW(
gtk_application_window_new(application));
gtk_window_present(context->main_window);
g_assert_true(create_person_dialog_present(context->main_window,
records, "/tmp", context->task_manager, NULL, completed,
context, NULL));
g_ptr_array_unref(records);
g_idle_add(click_cancel, context);
}
int main(int argc, char **argv)
{
TestContext context = {0};
int status;
if (!gtk_init_check()) {
g_print("SKIP: aucun affichage GTK disponible.\n");
return 0;
}
context.application = gtk_application_new(
"org.labfy.Investigation.DialogTest",
G_APPLICATION_NON_UNIQUE);
context.task_manager = task_manager_new();
g_signal_connect(context.application, "activate",
G_CALLBACK(activate), &context);
status = g_application_run(
G_APPLICATION(context.application), argc, argv);
g_assert_true(context.passed);
task_manager_free(context.task_manager);
g_object_unref(context.application);
return status;
}

View file

@ -525,7 +525,8 @@ static void test_database_initialize_valid_database(void)
"FROM investigation;" "FROM investigation;"
); );
assert(strcmp(schema_version, "13") == 0); assert(strcmp(schema_version, "14") == 0);
test_database_assert_table_exists(database, "person_role_assignments");
test_database_assert_table_exists(database, "bank_account_entities"); test_database_assert_table_exists(database, "bank_account_entities");
test_database_assert_table_exists(database, "relation_types"); test_database_assert_table_exists(database, "relation_types");
test_database_assert_table_exists(database, "graph_viewport"); test_database_assert_table_exists(database, "graph_viewport");
@ -992,7 +993,7 @@ static void test_database_migrate_v1_to_v2(void)
assert( assert(
strcmp( strcmp(
schema_version, schema_version,
"13" "14"
) == 0 ) == 0
); );
@ -1412,7 +1413,17 @@ static void test_database_migrate_v12_to_v13_preserves_legacy_link(void)
assert(database_initialize(path, "Migration V13 synthétique", directory)); assert(database_initialize(path, "Migration V13 synthétique", directory));
assert(sqlite3_open(path, &sqlite_database) == SQLITE_OK); assert(sqlite3_open(path, &sqlite_database) == SQLITE_OK);
test_database_execute_sql(sqlite_database, test_database_execute_sql(sqlite_database,
"INSERT INTO entites(id,type_id,valeur,label,confiance,created_at,"
"updated_at,status) VALUES("
"'20000000-0000-4000-8000-000000000014',"
"(SELECT id FROM types_entite WHERE code='person'),"
"'Personne historique','Personne historique',50,"
"'2026-07-28T08:00:00Z','2026-07-28T08:00:00Z','active');"
"INSERT INTO person_roles(entity_id,role,updated_at) VALUES("
"'20000000-0000-4000-8000-000000000014','alleged_scammer',"
"'2026-07-28T08:00:00Z');"
"DROP TABLE preuve_entite_sources;" "DROP TABLE preuve_entite_sources;"
"DROP TABLE person_role_assignments;"
"UPDATE metadata SET value='12' WHERE key='schema_version';" "UPDATE metadata SET value='12' WHERE key='schema_version';"
"INSERT INTO preuves(id,name,relative_path,type_id,size_bytes,sha256," "INSERT INTO preuves(id,name,relative_path,type_id,size_bytes,sha256,"
"imported_at,updated_at,status,locked,original_name) VALUES(" "imported_at,updated_at,status,locked,original_name) VALUES("
@ -1437,11 +1448,17 @@ static void test_database_migrate_v12_to_v13_preserves_legacy_link(void)
legacy_sources = test_database_read_single_text(sqlite_database, legacy_sources = test_database_read_single_text(sqlite_database,
"SELECT COUNT(*) FROM preuve_entite_sources " "SELECT COUNT(*) FROM preuve_entite_sources "
"WHERE source_kind='legacy_manual';"); "WHERE source_kind='legacy_manual';");
assert(strcmp(version, "13") == 0); assert(strcmp(version, "14") == 0);
assert(strcmp(legacy_sources, "1") == 0); assert(strcmp(legacy_sources, "1") == 0);
char *legacy_role = test_database_read_single_text(sqlite_database,
"SELECT role_code || ':' || provenance_kind "
"FROM person_role_assignments WHERE entity_id="
"'20000000-0000-4000-8000-000000000014';");
assert(strcmp(legacy_role, "alleged_scammer:legacy_manual") == 0);
assert(sqlite3_close(sqlite_database) == SQLITE_OK); assert(sqlite3_close(sqlite_database) == SQLITE_OK);
assert(g_remove(path) == 0 && g_rmdir(directory) == 0); assert(g_remove(path) == 0 && g_rmdir(directory) == 0);
g_free(legacy_sources); g_free(legacy_sources);
g_free(legacy_role);
g_free(version); g_free(version);
g_free(path); g_free(path);
g_free(directory); g_free(directory);

View file

@ -0,0 +1,122 @@
#include "core/evidence_preview.h"
#include "core/file_hash.h"
#include <cairo.h>
#include <stdio.h>
#include <jpeglib.h>
#include "core/evidence_preview_task.h"
#include <glib/gstdio.h>
static void test_preview_png_and_guards(void)
{
GError *error = NULL;
char *root = g_dir_make_tmp("labfy-preview-test-XXXXXX", &error);
char *path = g_build_filename(root, "SPECIMEN.png", NULL);
cairo_surface_t *fixture = cairo_image_surface_create(
CAIRO_FORMAT_RGB24, 32, 20);
cairo_t *cr = cairo_create(fixture);
char *sha256 = NULL;
guint64 size_bytes = 0;
EvidencePreviewRequest *request;
EvidencePreviewResult *result;
g_assert_nonnull(root);
cairo_set_source_rgb(cr, 0.2, 0.4, 0.6);
cairo_paint(cr);
g_assert_cmpint(cairo_surface_write_to_png(fixture, path), ==,
CAIRO_STATUS_SUCCESS);
g_assert_true(file_hash_compute_sha256(
path, NULL, &sha256, &size_bytes, &error));
request = evidence_preview_request_new(root,
"10000000-0000-4000-8000-000000000109", "SPECIMEN.png",
sha256, "image/png", 7);
result = evidence_preview_load(request, NULL, &error);
g_assert_no_error(error);
g_assert_nonnull(result);
g_assert_true(evidence_preview_result_matches(result,
"10000000-0000-4000-8000-000000000109", 7));
g_assert_false(evidence_preview_result_matches(result,
"10000000-0000-4000-8000-000000000109", 8));
evidence_preview_result_free(result);
g_free(request->expected_sha256);
request->expected_sha256 = g_strdup(
"0000000000000000000000000000000000000000000000000000000000000000");
result = evidence_preview_load(request, NULL, &error);
g_assert_null(result);
g_assert_error(error, g_quark_from_static_string(
"evidence-preview-error"), 2);
g_clear_error(&error);
evidence_preview_request_free(request);
cairo_destroy(cr);
cairo_surface_destroy(fixture);
g_assert_cmpint(g_remove(path), ==, 0);
g_assert_cmpint(g_rmdir(root), ==, 0);
g_free(sha256); g_free(path); g_free(root);
}
static void write_jpeg(const char *path)
{
struct jpeg_compress_struct compressor;
struct jpeg_error_mgr errors;
FILE *output = fopen(path, "wb");
JSAMPLE row[3 * 32];
g_assert_nonnull(output);
memset(row, 0x80, sizeof(row));
compressor.err = jpeg_std_error(&errors);
jpeg_create_compress(&compressor);
jpeg_stdio_dest(&compressor, output);
compressor.image_width = 32; compressor.image_height = 20;
compressor.input_components = 3; compressor.in_color_space = JCS_RGB;
jpeg_set_defaults(&compressor); jpeg_start_compress(&compressor, TRUE);
while (compressor.next_scanline < compressor.image_height) {
JSAMPROW rows[] = {row};
jpeg_write_scanlines(&compressor, rows, 1);
}
jpeg_finish_compress(&compressor); jpeg_destroy_compress(&compressor);
fclose(output);
}
typedef struct { GMainLoop *loop; gboolean completed; } AsyncState;
static void preview_done(BackgroundTask *task, gpointer data)
{
AsyncState *state = data;
state->completed = background_task_get_state(task) ==
BACKGROUND_TASK_STATE_COMPLETED;
g_main_loop_quit(state->loop);
}
static void test_preview_jpeg_async(void)
{
GError *error = NULL;
char *root = g_dir_make_tmp("labfy-preview-jpeg-XXXXXX", &error);
char *path = g_build_filename(root, "SPECIMEN.jpg", NULL);
char *sha = NULL; guint64 size = 0;
TaskManager *manager = task_manager_new();
AsyncState state = {.loop = g_main_loop_new(NULL, FALSE)};
write_jpeg(path);
g_assert_true(file_hash_compute_sha256(path, NULL, &sha, &size, &error));
EvidencePreviewRequest *request = evidence_preview_request_new(root,
"10000000-0000-4000-8000-000000000110", "SPECIMEN.jpg",
sha, "image/jpeg", 9);
BackgroundTask *task = evidence_preview_task_start(manager, request,
preview_done, &state, NULL, &error);
g_assert_nonnull(task);
g_main_loop_run(state.loop);
g_assert_true(state.completed);
EvidencePreviewResult *result = background_task_get_result(task);
g_assert_nonnull(result);
g_assert_cmpint(result->width, ==, 32);
g_assert_cmpint(result->height, ==, 20);
background_task_unref(task);
evidence_preview_request_free(request);
task_manager_free(manager); g_main_loop_unref(state.loop);
g_assert_cmpint(g_remove(path), ==, 0); g_assert_cmpint(g_rmdir(root), ==, 0);
g_free(sha); g_free(path); g_free(root);
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
g_test_add_func("/evidence-preview/png-and-guards",
test_preview_png_and_guards);
g_test_add_func("/evidence-preview/jpeg-async",
test_preview_jpeg_async);
return g_test_run();
}

View file

@ -0,0 +1,109 @@
#include "models/evidence_selection_model.h"
#include <glib.h>
static EvidenceRecord *record(const char *id, const char *name,
const char *type, const char *label, const char *description)
{
GError *error = NULL;
EvidenceRecord *r = evidence_record_new(id, name, "synthetic.bin",
"01_Preuves_Originales/synthetic.bin", type, 12,
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"2026-07-28T10:00:00Z", NULL, NULL, description,
EVIDENCE_INTEGRITY_STATUS_UNKNOWN, &error);
g_assert_no_error(error);
evidence_record_set_display_metadata(r, label, "image/png");
return r;
}
static void test_search_filter_selection(void)
{
GPtrArray *records = g_ptr_array_new_with_free_func(
(GDestroyNotify) evidence_record_free);
g_ptr_array_add(records, record(
"10000000-0000-4000-8000-000000000001", "SPECIMEN Alpha.PNG",
"screenshot", "Capture d'écran", "Écran de démonstration"));
g_ptr_array_add(records, record(
"10000000-0000-4000-8000-000000000002", "specimen-beta.jpg",
"photo", "Photographie", "Portrait entièrement synthétique"));
EvidenceSelectionModel *model = evidence_selection_model_new(records);
GPtrArray *visible = evidence_selection_model_list_visible(model);
g_assert_cmpuint(visible->len, ==, 2); g_ptr_array_unref(visible);
g_assert_true(evidence_selection_model_select(model,
"10000000-0000-4000-8000-000000000001"));
evidence_selection_model_set_query(model, "ÉCRAN");
visible = evidence_selection_model_list_visible(model);
g_assert_cmpuint(visible->len, ==, 1); g_ptr_array_unref(visible);
g_assert_nonnull(evidence_selection_model_get_selected(model));
evidence_selection_model_set_type(model, "photo");
g_assert_null(evidence_selection_model_get_selected(model));
visible = evidence_selection_model_list_visible(model);
g_assert_cmpuint(visible->len, ==, 0); g_ptr_array_unref(visible);
evidence_selection_model_set_query(model, "");
visible = evidence_selection_model_list_visible(model);
g_assert_cmpuint(visible->len, ==, 1); g_ptr_array_unref(visible);
evidence_selection_model_set_type(model, NULL);
evidence_selection_model_set_query(model, "capture");
visible = evidence_selection_model_list_visible(model);
g_assert_cmpuint(visible->len, ==, 1); g_ptr_array_unref(visible);
evidence_selection_model_free(model);
g_ptr_array_unref(records);
}
static void test_owned_records_survive_source(void)
{
GPtrArray *records = g_ptr_array_new_with_free_func(
(GDestroyNotify) evidence_record_free);
g_ptr_array_add(records, record(
"10000000-0000-4000-8000-000000000003",
"SPECIMEN durée de vie.png", "screenshot",
"Capture décran", NULL));
EvidenceSelectionModel *model = evidence_selection_model_new(records);
g_ptr_array_unref(records);
evidence_selection_model_set_query(model, "d");
GPtrArray *visible = evidence_selection_model_list_visible(model);
g_assert_cmpuint(visible->len, ==, 1);
g_ptr_array_unref(visible);
evidence_selection_model_free(model);
}
static void test_null_utf8_and_successive_queries(void)
{
GPtrArray *records = g_ptr_array_new_with_free_func(
(GDestroyNotify) evidence_record_free);
EvidenceRecord *without_optional = record(
"10000000-0000-4000-8000-000000000004",
"SPECIMEN été.png", "screenshot", "Capture française", NULL);
evidence_record_set_display_metadata(
without_optional, "Capture française", NULL);
g_ptr_array_add(records, without_optional);
EvidenceSelectionModel *model = evidence_selection_model_new(records);
const char *queries[] = {
NULL, "", "é", "ÉTÉ", "capture", "screenshot",
"image/png", "不存在", "a", "ab", "abc", NULL
};
for (guint i = 0; i < G_N_ELEMENTS(queries); i++) {
evidence_selection_model_set_query(model, queries[i]);
GPtrArray *visible = evidence_selection_model_list_visible(model);
g_assert_nonnull(visible);
g_ptr_array_unref(visible);
}
char *long_query = g_strnfill(4096, 'x');
evidence_selection_model_set_query(model, long_query);
GPtrArray *visible = evidence_selection_model_list_visible(model);
g_assert_cmpuint(visible->len, ==, 0);
g_ptr_array_unref(visible);
g_free(long_query);
evidence_selection_model_free(model);
g_ptr_array_unref(records);
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
g_test_add_func("/evidence-selection/search-filter",
test_search_filter_selection);
g_test_add_func("/evidence-selection/owned-records",
test_owned_records_survive_source);
g_test_add_func("/evidence-selection/null-utf8-successive",
test_null_utf8_and_successive_queries);
return g_test_run();
}

View file

@ -0,0 +1,60 @@
#include "core/person_confirmation_summary.h"
#include <glib.h>
static EvidenceRecord *record(void)
{
EvidenceRecord *evidence = evidence_record_new(
"10000000-0000-4000-8000-000000000010",
"SPECIMEN-confirmation.png", "specimen.png",
"01_Preuves_Originales/specimen.png", "screenshot", 42,
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"2026-07-28T10:00:00Z", NULL, NULL, "Description SPECIMEN",
EVIDENCE_INTEGRITY_STATUS_VALID, NULL);
evidence_record_set_display_metadata(
evidence, "Capture décran", "image/png");
return evidence;
}
static void test_full_summary(void)
{
EvidenceRecord *evidence = record();
GPtrArray *roles = g_ptr_array_new();
g_ptr_array_add(roles, "Victime");
g_ptr_array_add(roles, "Témoin");
char *summary = person_confirmation_summary_build(
"Personne SPECIMEN", "Nom SPECIMEN", "Pseudo", "Présumé", 55,
"Notes synthétiques", roles, evidence);
const char *values[] = {
"Personne SPECIMEN", "Nom SPECIMEN", "Pseudo", "Présumé", "55 %",
"Victime", "Témoin", "SPECIMEN-confirmation.png",
"Capture décran", "image/png", "42", "2026-07-28T10:00:00Z",
"aaaaaaaaaaaa…", "Valide", "Description SPECIMEN",
"Aucune écriture na encore été effectuée"
};
for (guint i = 0; i < G_N_ELEMENTS(values); i++)
g_assert_nonnull(strstr(summary, values[i]));
g_free(summary);
g_ptr_array_unref(roles);
evidence_record_free(evidence);
}
static void test_optional_summary(void)
{
char *summary = person_confirmation_summary_build(
"SPECIMEN", "", NULL, "Inconnu", 0, "", NULL, NULL);
g_assert_nonnull(strstr(summary, "Nom déclaré : Non renseigné"));
g_assert_nonnull(strstr(summary, "Pseudonyme : Non renseigné"));
g_assert_nonnull(strstr(summary, "Notes factuelles : Aucune"));
g_assert_nonnull(strstr(summary, "Aucun rôle sélectionné"));
g_assert_nonnull(strstr(summary,
"Aucune preuve principale associée"));
g_free(summary);
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
g_test_add_func("/person-confirmation/full", test_full_summary);
g_test_add_func("/person-confirmation/optional", test_optional_summary);
return g_test_run();
}

View file

@ -0,0 +1,24 @@
#include "core/person_creation_guard.h"
#include <glib.h>
static void test_guard(void)
{
PersonCreationGuard *guard = person_creation_guard_new(3,
"/tmp/synthetic-a", "/tmp/synthetic-a/Enquete.sqlite");
g_assert_true(person_creation_guard_matches(guard, TRUE, 3,
"/tmp/synthetic-a", "/tmp/synthetic-a/Enquete.sqlite"));
g_assert_false(person_creation_guard_matches(guard, TRUE, 4,
"/tmp/synthetic-a", "/tmp/synthetic-a/Enquete.sqlite"));
g_assert_false(person_creation_guard_matches(guard, TRUE, 3,
"/tmp/synthetic-a", "/tmp/synthetic-b/Enquete.sqlite"));
g_assert_false(person_creation_guard_matches(guard, FALSE, 3,
"/tmp/synthetic-a", "/tmp/synthetic-a/Enquete.sqlite"));
g_assert_false(person_creation_guard_matches(guard, TRUE, 3,
NULL, NULL));
person_creation_guard_free(guard);
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
g_test_add_func("/person-creation-guard/session", test_guard);
return g_test_run();
}

View file

@ -0,0 +1,117 @@
#include "core/person_dialog_lifecycle.h"
#include <glib.h>
typedef struct
{
guint callback_count;
guint person_count;
guint role_count;
guint attachment_count;
guint manual_source_count;
guint graph_refresh_count;
guint quit_count;
gboolean main_window_active;
} CancellationEffects;
static void cancel_once(PersonDialogLifecycle *lifecycle,
CancellationEffects *effects)
{
if (!person_dialog_lifecycle_cancel(lifecycle)) return;
effects->callback_count++;
}
static void assert_no_business_effect(const CancellationEffects *effects)
{
g_assert_cmpuint(effects->person_count, ==, 0);
g_assert_cmpuint(effects->role_count, ==, 0);
g_assert_cmpuint(effects->attachment_count, ==, 0);
g_assert_cmpuint(effects->manual_source_count, ==, 0);
g_assert_cmpuint(effects->graph_refresh_count, ==, 0);
g_assert_cmpuint(effects->quit_count, ==, 0);
g_assert_true(effects->main_window_active);
}
static void test_cancel_on_each_step(void)
{
for (guint step = 0; step < 4; step++) {
PersonDialogLifecycle *lifecycle = person_dialog_lifecycle_new();
CancellationEffects effects = {.main_window_active = TRUE};
cancel_once(lifecycle, &effects);
g_assert_true(person_dialog_lifecycle_is_finished(lifecycle));
g_assert_true(person_dialog_lifecycle_is_cancelled(lifecycle));
g_assert_cmpuint(effects.callback_count, ==, 1);
assert_no_business_effect(&effects);
person_dialog_lifecycle_free(lifecycle);
}
}
static void test_close_and_double_cancel_are_idempotent(void)
{
PersonDialogLifecycle *lifecycle = person_dialog_lifecycle_new();
CancellationEffects effects = {.main_window_active = TRUE};
cancel_once(lifecycle, &effects);
cancel_once(lifecycle, &effects);
g_assert_false(person_dialog_lifecycle_cancel(lifecycle));
g_assert_false(person_dialog_lifecycle_complete(lifecycle));
g_assert_cmpuint(effects.callback_count, ==, 1);
assert_no_business_effect(&effects);
person_dialog_lifecycle_free(lifecycle);
}
static void test_preview_is_cancelled_and_late_result_ignored(void)
{
PersonDialogLifecycle *lifecycle = person_dialog_lifecycle_new();
CancellationEffects effects = {.main_window_active = TRUE};
guint64 generation =
person_dialog_lifecycle_begin_preview(lifecycle);
g_assert_true(person_dialog_lifecycle_has_preview(lifecycle));
g_assert_true(person_dialog_lifecycle_accepts_preview(
lifecycle, generation));
cancel_once(lifecycle, &effects);
g_assert_false(person_dialog_lifecycle_has_preview(lifecycle));
g_assert_false(person_dialog_lifecycle_accepts_preview(
lifecycle, generation));
g_assert_cmpuint(person_dialog_lifecycle_get_generation(lifecycle),
>, generation);
assert_no_business_effect(&effects);
person_dialog_lifecycle_free(lifecycle);
}
static void test_complete_once(void)
{
PersonDialogLifecycle *lifecycle = person_dialog_lifecycle_new();
g_assert_true(person_dialog_lifecycle_complete(lifecycle));
g_assert_true(person_dialog_lifecycle_is_finished(lifecycle));
g_assert_false(person_dialog_lifecycle_is_cancelled(lifecycle));
g_assert_false(person_dialog_lifecycle_complete(lifecycle));
g_assert_false(person_dialog_lifecycle_cancel(lifecycle));
person_dialog_lifecycle_free(lifecycle);
}
static void test_null_and_full_destruction(void)
{
g_assert_false(person_dialog_lifecycle_cancel(NULL));
g_assert_false(person_dialog_lifecycle_complete(NULL));
g_assert_false(person_dialog_lifecycle_accepts_preview(NULL, 1));
person_dialog_lifecycle_free(NULL);
for (guint i = 0; i < 128; i++) {
PersonDialogLifecycle *lifecycle = person_dialog_lifecycle_new();
person_dialog_lifecycle_begin_preview(lifecycle);
person_dialog_lifecycle_cancel(lifecycle);
person_dialog_lifecycle_free(lifecycle);
}
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
g_test_add_func("/person-dialog/cancel-each-step",
test_cancel_on_each_step);
g_test_add_func("/person-dialog/close-idempotent",
test_close_and_double_cancel_are_idempotent);
g_test_add_func("/person-dialog/late-preview",
test_preview_is_cancelled_and_late_result_ignored);
g_test_add_func("/person-dialog/complete-once", test_complete_once);
g_test_add_func("/person-dialog/destruction", test_null_and_full_destruction);
return g_test_run();
}

View file

@ -5,7 +5,9 @@
#include "core/person_entity_service.h" #include "core/person_entity_service.h"
#include "database/database.h" #include "database/database.h"
#include "dao/entity_dao.h" #include "dao/entity_dao.h"
#include "dao/person_role_assignment_dao.h"
#include "models/entity_record.h" #include "models/entity_record.h"
#include "database/statement.h"
#include <assert.h> #include <assert.h>
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
@ -25,6 +27,15 @@ static void test_person_entity_create(void)
.identification_status = "suspected", .identification_status = "suspected",
.notes = "Identité non confirmée.", .confidence = 30, .notes = "Identité non confirmée.", .confidence = 30,
.evidence_identifier = NULL}; .evidence_identifier = NULL};
PersonRoleAssignmentInput victim = {
.role_code = "victim", .provenance_kind = "manual",
.has_confidence = TRUE, .confidence = 70};
PersonRoleAssignmentInput witness = {
.role_code = "witness", .provenance_kind = "manual"};
GPtrArray *roles = g_ptr_array_new();
g_ptr_array_add(roles, &victim);
g_ptr_array_add(roles, &witness);
input.role_assignments = roles;
directory = g_dir_make_tmp("labfy-person-service-test-XXXXXX", &error); directory = g_dir_make_tmp("labfy-person-service-test-XXXXXX", &error);
assert(directory != NULL && error == NULL); assert(directory != NULL && error == NULL);
path = g_build_filename(directory, "Enquete.sqlite", NULL); path = g_build_filename(directory, "Enquete.sqlite", NULL);
@ -37,6 +48,13 @@ static void test_person_entity_create(void)
assert(record != NULL && error == NULL); assert(record != NULL && error == NULL);
assert(strcmp(entity_record_get_type_identifier(record), "person") == 0); assert(strcmp(entity_record_get_type_identifier(record), "person") == 0);
assert(entity_record_get_confidence(record) == 30); assert(entity_record_get_confidence(record) == 30);
PersonRoleAssignmentDao *role_dao =
person_role_assignment_dao_new(database, &error);
GPtrArray *saved_roles = person_role_assignment_dao_list_by_entity(
role_dao, identifier, &error);
assert(saved_roles != NULL && saved_roles->len == 2);
g_ptr_array_unref(saved_roles);
person_role_assignment_dao_free(role_dao);
assert(entity_record_get_person_role(record) == PERSON_ROLE_UNCATEGORIZED); assert(entity_record_get_person_role(record) == PERSON_ROLE_UNCATEGORIZED);
entity_record_free(record); record = NULL; entity_record_free(record); record = NULL;
assert(person_entity_service_update_role(database, identifier, assert(person_entity_service_update_role(database, identifier,
@ -63,10 +81,72 @@ static void test_person_entity_create(void)
assert(strcmp(entity_record_get_label(record), "Vraie identité") == 0); assert(strcmp(entity_record_get_label(record), "Vraie identité") == 0);
entity_record_free(record); entity_dao_free(dao); database_close(database); entity_record_free(record); entity_dao_free(dao); database_close(database);
assert(g_remove(path) == 0); assert(g_rmdir(directory) == 0); assert(g_remove(path) == 0); assert(g_rmdir(directory) == 0);
g_ptr_array_unref(roles);
g_free(identifier); g_free(path); g_free(directory); g_free(identifier); g_free(path); g_free(directory);
} }
static gint64 count_rows(Database *database, const char *sql)
{
DatabaseStatement *statement = database_statement_prepare(database, sql);
int64_t count = -1;
assert(statement != NULL);
assert(database_statement_step(statement) == DATABASE_STATEMENT_STEP_ROW);
assert(database_statement_column_int64(statement, 0, &count));
database_statement_finalize(statement);
return count;
}
static void test_person_entity_rollbacks(void)
{
GError *error = NULL;
char *directory = g_dir_make_tmp("labfy-person-rollback-XXXXXX", &error);
char *path = g_build_filename(directory, "Enquete.sqlite", NULL);
Database *database;
PersonRoleAssignmentInput first = {
.role_code="victim", .provenance_kind="manual"};
PersonRoleAssignmentInput duplicate = {
.role_code="victim", .provenance_kind="manual"};
GPtrArray *roles = g_ptr_array_new();
PersonEntityInput input = {
.designation="Personne rollback SPECIMEN",
.identification_status="unknown", .confidence=20,
.role_assignments=roles};
g_assert_true(database_initialize(path, "Rollback synthétique", directory));
database = database_open(path);
g_assert_true(database_migrate_to_latest(database));
g_ptr_array_add(roles, &first); g_ptr_array_add(roles, &duplicate);
g_assert_false(person_entity_service_create(
database, &input, NULL, &error));
g_clear_error(&error);
g_assert_cmpint(count_rows(database,
"SELECT COUNT(*) FROM entites WHERE label='Personne rollback SPECIMEN';"),
==, 0);
g_assert_cmpint(count_rows(database,
"SELECT COUNT(*) FROM person_role_assignments;"), ==, 0);
g_ptr_array_set_size(roles, 1);
input.evidence_identifier =
"10000000-0000-4000-8000-000000000999";
first.evidence_identifier = NULL;
g_assert_false(person_entity_service_create(
database, &input, NULL, &error));
g_clear_error(&error);
g_assert_cmpint(count_rows(database,
"SELECT COUNT(*) FROM entites WHERE label='Personne rollback SPECIMEN';"),
==, 0);
g_assert_cmpint(count_rows(database,
"SELECT COUNT(*) FROM person_role_assignments;"), ==, 0);
g_assert_cmpint(count_rows(database,
"SELECT COUNT(*) FROM preuve_entites;"), ==, 0);
g_assert_cmpint(count_rows(database,
"SELECT COUNT(*) FROM preuve_entite_sources;"), ==, 0);
g_ptr_array_unref(roles); database_close(database);
g_assert_cmpint(g_remove(path), ==, 0);
g_assert_cmpint(g_rmdir(directory), ==, 0);
g_free(path); g_free(directory);
}
int main(void) int main(void)
{ {
test_person_entity_create(); test_person_entity_create();
test_person_entity_rollbacks();
puts("PersonEntityService : tous les tests sont valides."); return 0; puts("PersonEntityService : tous les tests sont valides."); return 0;
} }

View file

@ -0,0 +1,113 @@
#include "dao/person_role_assignment_dao.h"
#include "dao/entity_dao.h"
#include "dao/evidence_dao.h"
#include "database/transaction.h"
#include <glib.h>
#include <glib/gstdio.h>
static EntityRecord *make_person(const char *id)
{
GError *error = NULL;
EntityRecord *r = entity_record_new(id, "person", id,
"Personne SPECIMEN", NULL, 50, "2026-07-28T10:00:00Z",
"2026-07-28T10:00:00Z", ENTITY_STATUS_ACTIVE, &error);
g_assert_no_error(error); return r;
}
static EvidenceRecord *make_evidence(const char *id, const char *name)
{
GError *error = NULL;
EvidenceRecord *r = evidence_record_new(id, name, name,
name, "document", 1,
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"2026-07-28T10:00:00Z", NULL, NULL, NULL,
EVIDENCE_INTEGRITY_STATUS_UNKNOWN, &error);
g_assert_no_error(error); return r;
}
static void test_dao_roles(void)
{
GError *error = NULL;
char *dir = g_dir_make_tmp("labfy-role-dao-XXXXXX", &error);
char *path = g_build_filename(dir, "Enquete.sqlite", NULL);
Database *db; EntityDao *entities; EvidenceDao *evidences;
PersonRoleAssignmentDao *roles; EntityRecord *person;
EvidenceRecord *evidence;
const char *person_id = "20000000-0000-4000-8000-000000000109";
const char *evidence_id = "10000000-0000-4000-8000-000000000109";
const char *evidence_id_2 = "10000000-0000-4000-8000-000000000110";
g_assert_true(database_initialize(path, "Rôles synthétiques", dir));
db = database_open(path); g_assert_true(database_migrate_to_latest(db));
entities = entity_dao_new(db, &error); evidences = evidence_dao_new(db, &error);
roles = person_role_assignment_dao_new(db, &error);
person = make_person(person_id); g_assert_true(entity_dao_insert(entities, person, &error));
evidence = make_evidence(evidence_id, "specimen.bin");
g_assert_true(evidence_dao_insert(evidences, evidence, &error));
EvidenceRecord *evidence_2 = make_evidence(
evidence_id_2, "specimen-2.bin");
g_assert_true(evidence_dao_insert(evidences, evidence_2, &error));
PersonRoleAssignmentInput without = {
.role_code="victim", .provenance_kind="manual",
.has_confidence=TRUE, .confidence=0};
PersonRoleAssignmentInput with = {
.role_code="witness", .evidence_identifier=(char *) evidence_id,
.provenance_kind="manual", .has_confidence=TRUE, .confidence=100};
g_assert_true(person_role_assignment_dao_insert(
roles, person_id, &without, &error));
g_assert_true(person_role_assignment_dao_insert(
roles, person_id, &with, &error));
PersonRoleAssignmentInput same_other_proof = with;
same_other_proof.evidence_identifier = (char *) evidence_id_2;
g_assert_true(person_role_assignment_dao_insert(
roles, person_id, &same_other_proof, &error));
GPtrArray *saved = person_role_assignment_dao_list_by_entity(
roles, person_id, &error);
g_assert_cmpuint(saved->len, ==, 3);
g_ptr_array_unref(saved);
PersonRoleAssignmentInput invalid = {
.role_code="unknown-code", .provenance_kind="manual"};
g_assert_false(person_role_assignment_dao_insert(
roles, person_id, &invalid, &error)); g_clear_error(&error);
invalid.role_code="victim"; invalid.has_confidence=TRUE; invalid.confidence=-1;
g_assert_false(person_role_assignment_dao_insert(
roles, person_id, &invalid, &error)); g_clear_error(&error);
invalid.confidence=101;
g_assert_false(person_role_assignment_dao_insert(
roles, person_id, &invalid, &error)); g_clear_error(&error);
with.evidence_identifier="10000000-0000-4000-8000-000000000999";
g_assert_false(person_role_assignment_dao_insert(
roles, person_id, &with, &error)); g_clear_error(&error);
with.evidence_identifier=(char *) evidence_id;
g_assert_false(person_role_assignment_dao_insert(roles,
"20000000-0000-4000-8000-000000000999", &with, &error));
g_clear_error(&error);
const char *rollback_person =
"20000000-0000-4000-8000-000000000110";
EntityRecord *person_2 = make_person(rollback_person);
g_assert_true(entity_dao_insert(entities, person_2, &error));
GPtrArray *batch = g_ptr_array_new();
PersonRoleAssignmentInput batch_valid = {
.role_code="intermediary", .provenance_kind="manual"};
PersonRoleAssignmentInput batch_invalid = {
.role_code="not-known", .provenance_kind="manual"};
g_ptr_array_add(batch, &batch_valid); g_ptr_array_add(batch, &batch_invalid);
g_assert_true(database_transaction_begin(db));
g_assert_false(person_role_assignment_dao_insert_all(
roles, rollback_person, batch, &error));
g_clear_error(&error);
g_assert_true(database_transaction_rollback(db));
saved = person_role_assignment_dao_list_by_entity(
roles, rollback_person, &error);
g_assert_cmpuint(saved->len, ==, 0);
g_ptr_array_unref(saved); g_ptr_array_unref(batch);
evidence_record_free(evidence_2); entity_record_free(person_2);
evidence_record_free(evidence); entity_record_free(person);
person_role_assignment_dao_free(roles); evidence_dao_free(evidences);
entity_dao_free(entities); database_close(db);
g_assert_cmpint(g_remove(path), ==, 0); g_assert_cmpint(g_rmdir(dir), ==, 0);
g_free(path); g_free(dir);
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
g_test_add_func("/person-role-assignment-dao/direct", test_dao_roles);
return g_test_run();
}