feat(person): ajouter le parcours multi-preuves et les aperçus partagés

This commit is contained in:
grayTerminal-sh 2026-07-28 13:43:27 +02:00
parent dfe6afcfca
commit b0ae90090a
48 changed files with 4103 additions and 627 deletions

View file

@ -1,6 +1,10 @@
CC = gcc
PKG_CONFIG = pkg-config
REQUIRED_PACKAGES = gtk4 sqlite3 libheif poppler-glib
ifeq ($(shell $(PKG_CONFIG) --exists $(REQUIRED_PACKAGES) && echo yes),)
$(error Dépendances de compilation manquantes : $(REQUIRED_PACKAGES). Voir docs/DEPENDENCE.md)
endif
.DEFAULT_GOAL := all
@ -12,9 +16,9 @@ CFLAGS = -std=c17 \
-Iinclude \
-MMD \
-MP \
$(shell $(PKG_CONFIG) --cflags gtk4 sqlite3)
$(shell $(PKG_CONFIG) --cflags gtk4 sqlite3 libheif poppler-glib)
LDFLAGS = $(shell $(PKG_CONFIG) --libs gtk4 sqlite3) -ljpeg
LDFLAGS = $(shell $(PKG_CONFIG) --libs gtk4 sqlite3 libheif poppler-glib) -ljpeg
TEST_CFLAGS = -std=c17 \
-Wall \
@ -126,12 +130,17 @@ TEST_SOCIAL_ACCOUNT_SERVICE := tests/test_social_account_service
TEST_SOCIAL_PLATFORM := tests/test_social_platform
TEST_PERSON_ENTITY_SERVICE := tests/test_person_entity_service
TEST_EVIDENCE_PREVIEW := tests/test_evidence_preview
TEST_EVIDENCE_VIDEO_PREVIEW_CONTROLLER := tests/test_evidence_video_preview_controller
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_EVIDENCE_PREVIEW_WIDGET_GTK := tests/test_evidence_preview_widget_gtk
TEST_PERSON_CONFIRMATION_SUMMARY := tests/test_person_confirmation_summary
TEST_PERSON_ROLE_ASSIGNMENT_DAO := tests/test_person_role_assignment_dao
TEST_PERSON_EVIDENCE_SELECTION := tests/test_person_evidence_selection
TEST_EVIDENCE_STAGING := tests/test_evidence_staging
TEST_PERSON_CREATION_COORDINATOR := tests/test_person_creation_coordinator
TEST_EML_ANALYZER := tests/test_eml_analyzer
TEST_EML_INTEGRATION := tests/test_eml_integration
TEST_IBAN_ANALYZER := tests/test_iban_analyzer
@ -803,9 +812,15 @@ $(TEST_EVIDENCE_PREVIEW): tests/test_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
src/core/file_hash.c src/core/eml_analyzer.c \
src/core/eml_mime_extractor.c src/models/evidence_record.c
$(CC) $(CFLAGS) -Wpedantic $^ -o $@ $(LDFLAGS) -ljpeg
$(TEST_EVIDENCE_VIDEO_PREVIEW_CONTROLLER): \
tests/test_evidence_video_preview_controller.c \
src/core/evidence_video_preview_controller.c
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ $(TEST_LDFLAGS)
$(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)
@ -821,16 +836,32 @@ $(TEST_PERSON_DIALOG_LIFECYCLE): tests/test_person_dialog_lifecycle.c \
$(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_staging.c src/core/evidence_staging_task.c \
src/core/evidence_preview_task.c src/core/evidence_preview.c \
src/core/eml_analyzer.c src/core/eml_mime_extractor.c \
src/core/evidence_video_preview_controller.c \
src/widgets/evidence_preview_widget.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
src/models/person_role_assignment.c \
src/models/person_evidence_selection.c
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
$(TEST_EVIDENCE_PREVIEW_WIDGET_GTK): \
tests/test_evidence_preview_widget_gtk.c \
src/widgets/evidence_preview_widget.c \
src/core/evidence_preview_task.c src/core/evidence_preview.c \
src/core/eml_analyzer.c src/core/eml_mime_extractor.c \
src/core/evidence_video_preview_controller.c \
src/core/evidence_integrity_verifier.c src/core/file_hash.c \
src/core/background_task.c src/core/task_manager.c
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS) -ljpeg
$(TEST_PERSON_CONFIRMATION_SUMMARY): \
tests/test_person_confirmation_summary.c \
src/core/person_confirmation_summary.c src/models/evidence_record.c
src/core/person_confirmation_summary.c src/models/evidence_record.c \
src/models/person_evidence_selection.c
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ $(TEST_LDFLAGS)
$(TEST_PERSON_ROLE_ASSIGNMENT_DAO): \
@ -842,6 +873,28 @@ $(TEST_PERSON_ROLE_ASSIGNMENT_DAO): \
src/database/transaction.c src/database/error.c
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ $(TEST_LDFLAGS) -lsqlite3
$(TEST_PERSON_EVIDENCE_SELECTION): \
tests/test_person_evidence_selection.c \
src/models/person_evidence_selection.c src/models/evidence_record.c
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ $(TEST_LDFLAGS)
$(TEST_EVIDENCE_STAGING): tests/test_evidence_staging.c \
src/core/evidence_staging.c src/core/file_hash.c
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ $(TEST_LDFLAGS)
$(TEST_PERSON_CREATION_COORDINATOR): \
tests/test_person_creation_coordinator.c \
src/core/person_creation_coordinator.c src/core/evidence_staging.c \
src/core/file_hash.c \
src/models/person_evidence_selection.c src/models/evidence_record.c \
src/models/person_role_assignment.c src/models/entity_record.c \
src/models/evidence_observation.c \
src/dao/entity_dao.c src/dao/evidence_dao.c \
src/dao/evidence_entity_dao.c src/dao/person_role_assignment_dao.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
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
@ -949,12 +1002,17 @@ test: \
$(TEST_SOCIAL_PLATFORM) \
$(TEST_PERSON_ENTITY_SERVICE) \
$(TEST_EVIDENCE_PREVIEW) \
$(TEST_EVIDENCE_VIDEO_PREVIEW_CONTROLLER) \
$(TEST_EVIDENCE_SELECTION_MODEL) \
$(TEST_PERSON_CREATION_GUARD) \
$(TEST_PERSON_DIALOG_LIFECYCLE) \
$(TEST_CREATE_PERSON_DIALOG_GTK) \
$(TEST_EVIDENCE_PREVIEW_WIDGET_GTK) \
$(TEST_PERSON_CONFIRMATION_SUMMARY) \
$(TEST_PERSON_ROLE_ASSIGNMENT_DAO) \
$(TEST_PERSON_EVIDENCE_SELECTION) \
$(TEST_EVIDENCE_STAGING) \
$(TEST_PERSON_CREATION_COORDINATOR) \
$(TEST_EML_ANALYZER) \
$(TEST_EML_INTEGRATION) \
$(TEST_IBAN_ANALYZER) \
@ -1030,12 +1088,17 @@ test: \
@$(TEST_SOCIAL_PLATFORM)
@$(TEST_PERSON_ENTITY_SERVICE)
@$(TEST_EVIDENCE_PREVIEW)
@$(TEST_EVIDENCE_VIDEO_PREVIEW_CONTROLLER)
@$(TEST_EVIDENCE_SELECTION_MODEL)
@$(TEST_PERSON_CREATION_GUARD)
@$(TEST_PERSON_DIALOG_LIFECYCLE)
@$(TEST_CREATE_PERSON_DIALOG_GTK)
@$(TEST_EVIDENCE_PREVIEW_WIDGET_GTK)
@$(TEST_PERSON_CONFIRMATION_SUMMARY)
@$(TEST_PERSON_ROLE_ASSIGNMENT_DAO)
@$(TEST_PERSON_EVIDENCE_SELECTION)
@$(TEST_EVIDENCE_STAGING)
@$(TEST_PERSON_CREATION_COORDINATOR)
@$(TEST_EML_ANALYZER)
@$(TEST_EML_INTEGRATION)
@$(TEST_IBAN_ANALYZER)
@ -1118,12 +1181,17 @@ clean:
$(TEST_SOCIAL_PLATFORM) \
$(TEST_PERSON_ENTITY_SERVICE) \
$(TEST_EVIDENCE_PREVIEW) \
$(TEST_EVIDENCE_VIDEO_PREVIEW_CONTROLLER) \
$(TEST_EVIDENCE_SELECTION_MODEL) \
$(TEST_PERSON_CREATION_GUARD) \
$(TEST_PERSON_DIALOG_LIFECYCLE) \
$(TEST_CREATE_PERSON_DIALOG_GTK) \
$(TEST_EVIDENCE_PREVIEW_WIDGET_GTK) \
$(TEST_PERSON_CONFIRMATION_SUMMARY) \
$(TEST_PERSON_ROLE_ASSIGNMENT_DAO) \
$(TEST_PERSON_EVIDENCE_SELECTION) \
$(TEST_EVIDENCE_STAGING) \
$(TEST_PERSON_CREATION_COORDINATOR) \
$(TEST_EML_ANALYZER) \
$(TEST_EML_INTEGRATION) \
$(TEST_EXTRACTION_DROP_SERVICE) \

View file

@ -191,6 +191,14 @@ Le socle actuel comprend notamment :
graphe, puis retrait réversible conservant l'observation.
- propriété persistante V13 des rattachements preuve-entité : un retrait EML
ne supprime que la source de l'observation concernée.
- assistant de création dune personne avec sélection multiple de preuves
existantes et import multiple différé : les nouveaux fichiers sont copiés
dans un staging temporaire, qualifiés individuellement et ne deviennent
définitifs quaprès confirmation globale ;
- rattachement transactionnel de toutes les preuves retenues, avec rollback
SQLite et suppression compensatoire des copies définitives en cas déchec.
- aperçu asynchrone contrôlé PNG/JPEG, HEIC/HEIF, MP4/MOV, texte, EML et
première page PDF, sans OCR ni écriture SQLite ;
### Pivot EML

View file

@ -1,5 +1,25 @@
# Architecture
`EvidencePreviewWidget` est ladaptateur GTK unique de `EvidencePreview` pour
`CreatePersonDialog` et la fiche directe. Il encapsule les états, le rendu
EML/PDF/image/texte/vidéo, lannulation, la génération, la garde de session et
larrêt du média. Les deux écrans lui transmettent uniquement une source
interne ou de staging accompagnée du SHA-256 attendu.
## Aperçu contrôlé multi-format
`EvidencePreview` vérifie lempreinte du fichier interne ou de staging,
détecte son contenu et produit un résultat borné sans GTK. `BackgroundTask`
travaille hors du thread principal ; `CreatePersonDialog` y crée ensuite
textures, buffers et médias, rejette les générations périmées et arrête tout
média au changement ou à la fermeture. Laperçu ne persiste rien.
Le mode aperçu de `EmlMimeExtractor` réutilise le parcours récursif et les
décodages RFC 2047/2231 sans écrire les pièces jointes. Il préfère
`text/plain`, transforme un éventuel HTML en texte inerte et retourne
linventaire possédé. Le contrôleur vidéo indépendant de GTK orchestre
pause, retour au début, détachement et libération via des actions injectées.
> **Version :** 3.1
> **Dernière mise à jour :** 2026-07-28
> **Schéma SQLite courant :** V14
@ -406,6 +426,25 @@ leur preuve source.
## 10. Tâches asynchrones
### 10.1 Création dune personne avec preuves
`PersonEvidenceSelection` possède la collection ordonnée des preuves retenues
et distingue les preuves existantes des copies de staging. Il ne dépend ni de
GTK ni de SQLite. `EvidenceStaging` refuse les fichiers spéciaux et liens
symboliques, calcule lempreinte de la source et de sa copie, détecte le MIME
et nettoie les temporaires lors dun retrait ou dune annulation.
`PersonCreationCoordinator` valide toutes les empreintes avant écriture, crée
la personne, importe les nouvelles preuves et rattache toute la collection
dans une transaction SQLite. Les copies définitives réalisées avant un échec
sont supprimées après rollback. `PersonCreationTask` exécute cette orchestration
hors du thread GTK. Un changement de session annule les tâches et interdit le
rafraîchissement dune autre enquête.
Les aperçus PNG/JPEG utilisent exclusivement le fichier interne dune preuve
existante ou sa copie de staging. PDF, vidéo, EML et autres formats ne sont
pas analysés dans cette tranche.
Les tâches longues utilisent l'infrastructure de tâche d'arrière-plan et le
gestionnaire de tâches.

View file

@ -1,5 +1,13 @@
# Conventions de développement
Toute vue présentant une preuve réutilise `EvidencePreviewWidget` : aucun
parsing EML/PDF ou cycle média ne doit être dupliqué dans un écran. Les
callbacks asynchrones utilisent une génération et une référence faible.
Un aperçu utilise seulement la copie contrôlée et vérifiée. La signature et
le contenu priment sur le MIME historique et lextension. Son résultat ne
contient aucun objet GTK et un média ne démarre jamais automatiquement.
> **Version :** 2.1
> **Dernière mise à jour :** 2026-07-28
> **Projet :** Labfy Investigation
@ -265,7 +273,7 @@ présentation graphique.
La version courante du schéma est :
```text
V13
V14
```
Les scripts versionnés sont conservés dans :
@ -391,6 +399,10 @@ Interdit :
Une entrée externe peut être absolue pendant l'import, mais le chemin enregistré
dans l'enquête doit respecter le modèle prévu par le schéma.
Un import intégré à un assistant utilise dabord une copie de staging
temporaire vérifiée. Le fichier source nest ni renommé ni modifié et ne doit
plus être relu pour laperçu ou limport final après préparation.
### 7.2 Immutabilité des preuves originales
Une preuve originale n'est jamais modifiée.

View file

@ -1,8 +1,19 @@
# 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é.
`libjpeg` décode JPEG, `libheif` HEIC/HEIF et Poppler GLib rend la première
page PDF du ticket #109. PNG utilise Cairo et la vidéo `GtkMediaFile` avec le
backend GTK/GStreamer. Aucun outil documentaire nest lancé et aucun dérivé
nest persisté.
| Fonction | Arch Linux | Ubuntu/Debian |
| --- | --- | --- |
| HEIC/HEIF | `libheif` | `libheif-dev` |
| PDF natif | `poppler-glib` | `libpoppler-glib-dev` |
| Vidéo GTK | `gst-plugins-base`, `gst-plugins-good` | `gstreamer1.0-plugins-base`, `gstreamer1.0-plugins-good` |
Les codecs vidéo supplémentaires restent optionnels.
Le Makefile vérifie explicitement GTK4, SQLite, libheif et Poppler GLib avant
compilation et renvoie vers ce document si une dépendance manque.
Ce fichier recense les outils utilisés par LabFy Investigation et leur
installation. Il sera complété à chaque intégration dun nouvel outil.

View file

@ -1,5 +1,21 @@
# Guide de développement
La validation ciblée du composant partagé comprend
`test_evidence_preview_widget_gtk`, `test_evidence_preview`,
`test_evidence_video_preview_controller` et
`test_create_person_dialog_gtk`, ce dernier sous
`G_DEBUG=fatal-criticals`.
Les tests daperçu génèrent exclusivement des fixtures `SPECIMEN` temporaires.
Ils couvrent dispatch, limites, annulation et intégrité, puis le parcours GTK
réel avec `G_DEBUG=fatal-criticals`. Aucun aperçu ne lance OCR ou SQLite.
La fixture HEIC est une matrice RGB `SPECIMEN` encodée en mémoire par
libheif/HEVC. Le test HEIF remplace uniquement la marque compatible du
conteneur synthétique. Les transformations dorientation libheif restent
appliquées par le décodage standard ; seule limage principale est rendue et
le nombre dimages de premier niveau est exposé.
> **Version :** 2.1
> **Dernière mise à jour :** 2026-07-28
> **Projet :** Labfy Investigation
@ -240,6 +256,21 @@ make -j8 \
./tests/test_database
```
Tests ciblés de la tranche 2 du ticket #109 :
```sh
make -j8 \
tests/test_person_evidence_selection \
tests/test_evidence_staging \
tests/test_person_creation_coordinator \
tests/test_person_confirmation_summary \
tests/test_evidence_preview \
tests/test_create_person_dialog_gtk
```
Ces tests utilisent uniquement des fichiers `SPECIMEN`, des répertoires
temporaires et des bases SQLite temporaires.
La fixture manuelle est
`tests/fixtures/eml/manual_smoke_test.eml`. Elle est exclusivement
synthétique. Pour la validation GTK, créer une enquête neuve dans un

View file

@ -1,13 +1,21 @@
# Roadmap
La tranche 2 partage désormais laperçu entre lassistant personne et la
fiche directe. La validation manuelle des codecs installés reste nécessaire ;
elle ne vaut pas achèvement du ticket #109 et ninclut aucun OCR.
Laperçu contrôlé du ticket #109 couvre PNG/JPEG, HEIC/HEIF, MP4/MOV, texte,
EML passif et première page PDF. OCR et analyse avancée restent hors périmètre.
> **Dernière mise à jour :** 2026-07-28
> **État du projet :** développement actif
> **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.
Les tranches 1 et 2 du ticket #109 couvrent les rôles contextuels multiples,
la sélection de plusieurs preuves existantes, le staging et limport multiple
différé, ainsi que la création transactionnelle avec compensation des
fichiers. OCR, extraction de documents didentité et analyses documentaires
avancées restent hors périmètre.
> **Usage opérationnel :** non prêt pour la production
---

View file

@ -1,5 +1,11 @@
# Test manuel — ticket #109, tranche 1
> Le parcours « Preuve » est désormais nommé « Preuves » et accepte une
> sélection multiple ainsi quun import différé. Pour le scénario historique
> de tranche 1, sélectionner une seule preuve existante avec « Ajouter à la
> sélection ». Le protocole étendu se trouve dans
> `docs/TEST_MANUEL_TICKET_109_TRANCHE_2.md`.
Utiliser uniquement une enquête de démonstration et des fichiers synthétiques
marqués `SPECIMEN`.

View file

@ -0,0 +1,73 @@
# Test manuel — ticket #109, tranche 2
Comparer le même EML synthétique dans la fiche directe et dans lassistant :
From, To, Cc, Date, Subject, Message-ID, corps et inventaire détaillé des
pièces jointes doivent être identiques. Tester ensuite PNG/JPEG/HEIC/HEIF,
PDF, MP4/MOV et texte, les changements rapides, la fermeture pendant
chargement ou vidéo, la réouverture et louverture dune fiche personne.
Vérifier labsence de critique GTK, décriture SQLite et dOCR.
Utiliser exclusivement une enquête temporaire et des fichiers synthétiques
dont le nom ou le contenu porte `SPECIMEN`.
1. Créer une enquête temporaire et y importer préalablement deux preuves
synthétiques PNG/JPEG.
2. Ouvrir « Ajouter une personne », renseigner une désignation fictive et
sélectionner plusieurs rôles.
3. Dans « Preuves », rechercher puis ajouter les deux preuves existantes.
Vérifier quun second ajout est refusé sans modifier la preuve.
4. Choisir « Importer des fichiers » et sélectionner simultanément un PNG, un
JPEG et un PDF synthétiques. Vérifier que rien napparaît encore dans
`01_Preuves_Originales` ni dans SQLite.
5. Activer chaque élément retenu. Vérifier les aperçus PNG/JPEG et le message
explicite dindisponibilité du PDF.
6. Modifier individuellement les types métier, naviguer en arrière puis en
avant et vérifier la conservation de létat.
7. Retirer une preuve existante et un fichier nouveau. Vérifier que la preuve
existante reste dans lenquête et que la copie de staging retirée disparaît.
8. Sélectionner deux fois le même fichier, puis deux fichiers de même contenu.
Vérifier le message de doublon et lunicité de la sélection.
9. Ajouter un fichier identique à une preuve existante. Vérifier la
réutilisation explicite de cette preuve.
10. Atteindre Confirmation et contrôler la liste complète, les origines,
types, MIME, tailles, empreintes abrégées et compteurs.
11. Annuler puis vérifier labsence de nouvelle personne, preuve, rôle,
rattachement et copie définitive, ainsi que le nettoyage du staging.
12. Recommencer, puis fermer la fenêtre pendant le staging. Vérifier la
fermeture propre, le nettoyage et le maintien de lapplication.
13. Recommencer et changer denquête pendant une préparation ou une création.
Vérifier lannulation, labsence décriture dans la nouvelle enquête et
labsence de rafraîchissement tardif.
14. Confirmer normalement. Vérifier que toutes les preuves sont rattachées à
la personne avec une source `manual`, puis rouvrir lenquête.
15. Comparer les empreintes des fichiers sources avant et après le parcours.
Elles doivent être identiques.
16. Vérifier quaucun OCR ni outil documentaire avancé na été lancé.
17. Ouvrir la liste des preuves disponibles, choisir successivement deux
preuves, revenir à « Aucune preuve associée », activer une recherche et un
filtre, puis sélectionner de nouveau une preuve. Vérifier labsence de
crash et de critique GTK.
18. Ajouter une image JPEG historique synthétique dont le MIME enregistré est
NULL et le type métier `document`. Vérifier que le contenu JPEG intègre est
détecté, que laperçu apparaît et que « MIME détecté : image/jpeg » est
affiché sans modification SQLite.
19. Renommer un fichier texte synthétique avec lextension `.jpg`, puis
vérifier que laperçu est refusé malgré lextension. Refaire avec un MIME
historique `image/jpeg` incohérent et vérifier le même refus.
20. Modifier le type individuel de limage de `document` vers `email`.
Vérifier immédiatement la même valeur dans la ligne retenue, le sélecteur,
les métadonnées et le résumé après un aller-retour entre les étapes.
21. Tester des fixtures PNG, JPEG, HEIC, HEIF, MP4, MOV, EML, TXT, CSV, LOG,
Markdown et PDF. Vérifier limage, le lecteur sans lecture automatique,
le texte passif, les en-têtes EML et la première page PDF.
22. Tester format invalide, extension trompeuse et fichier trop volumineux.
23. Changer rapidement de preuve et vérifier le rejet tardif et larrêt vidéo.
24. Fermer puis changer denquête pendant un aperçu ; vérifier lannulation.
25. Vérifier les empreintes inchangées et labsence dOCR, daccès distant et
décriture SQLite pendant laperçu.
26. Avec un EML multipart synthétique, vérifier que le corps `text/plain` est
préféré au HTML et que nom, MIME, taille, disposition et Content-ID des
pièces jointes sont visibles sans fichier extrait persistant.
Limites : aucun OCR ni analyse avancée. La vidéo dépend des codecs GStreamer.

View file

@ -49,6 +49,9 @@ typedef struct EmlMimeResult
{
GPtrArray *attachments; /**< Tableau de EmlAttachment* */
GPtrArray *warnings; /**< Avertissements d'extraction */
char *body_text; /**< Corps choisi pour un aperçu passif. */
gboolean body_from_html; /**< VRAI si un repli HTML a été utilisé. */
gboolean body_truncated; /**< VRAI si le corps a été borné. */
} EmlMimeResult;
/**
@ -95,6 +98,10 @@ EmlMimeResult *eml_mime_extract_attachments_cancellable(
GError **error
);
/** Analyse MIME sans écrire les contenus décodés sur disque. */
EmlMimeResult *eml_mime_build_preview(const char *eml_path,
GCancellable *cancellable, GError **error);
G_END_DECLS
#endif /* LABFY_INVESTIGATION_EML_MIME_EXTRACTOR_H */

View file

@ -7,6 +7,16 @@ G_BEGIN_DECLS
#define EVIDENCE_PREVIEW_MAX_DIMENSION 12000
#define EVIDENCE_PREVIEW_MAX_PIXELS 40000000U
#define EVIDENCE_PREVIEW_MAX_EDGE 1024
#define EVIDENCE_PREVIEW_MAX_TEXT_BYTES (1024U * 1024U)
typedef enum {
EVIDENCE_PREVIEW_KIND_IMAGE,
EVIDENCE_PREVIEW_KIND_VIDEO,
EVIDENCE_PREVIEW_KIND_TEXT,
EVIDENCE_PREVIEW_KIND_EMAIL,
EVIDENCE_PREVIEW_KIND_PDF,
EVIDENCE_PREVIEW_KIND_UNSUPPORTED,
EVIDENCE_PREVIEW_KIND_ERROR
} EvidencePreviewKind;
typedef struct {
char *investigation_root_path;
char *evidence_identifier;
@ -16,11 +26,23 @@ typedef struct {
guint64 request_generation;
} EvidencePreviewRequest;
typedef struct {
EvidencePreviewKind kind;
char *evidence_identifier;
guint64 request_generation;
GBytes *png_bytes;
gint width;
gint height;
char *effective_mime_type;
char *effective_format;
char *display_name;
char *controlled_path;
char *text;
char *message;
guint64 size_bytes;
guint item_count;
gboolean integrity_valid;
gboolean preview_available;
gboolean truncated;
} EvidencePreviewResult;
EvidencePreviewRequest *evidence_preview_request_new(const char *root,
const char *evidence_identifier, const char *relative_path,
@ -32,5 +54,6 @@ EvidencePreviewResult *evidence_preview_load(
void evidence_preview_result_free(EvidencePreviewResult *result);
gboolean evidence_preview_result_matches(const EvidencePreviewResult *result,
const char *evidence_identifier, guint64 generation);
gboolean evidence_preview_dimensions_are_safe(gint width, gint height);
G_END_DECLS
#endif

View file

@ -0,0 +1,35 @@
#ifndef LABFY_INVESTIGATION_EVIDENCE_STAGING_H
#define LABFY_INVESTIGATION_EVIDENCE_STAGING_H
#include <gio/gio.h>
#include <glib.h>
G_BEGIN_DECLS
#define EVIDENCE_STAGING_MAX_BYTES (50U * 1024U * 1024U)
typedef struct EvidenceStaging EvidenceStaging;
typedef struct {
char *source_path;
char *staging_path;
char *original_name;
char *mime_type;
char *suggested_type;
char *sha256;
char *prepared_at;
guint64 size_bytes;
} EvidenceStagingResult;
EvidenceStaging *evidence_staging_new(GError **error);
EvidenceStaging *evidence_staging_ref(EvidenceStaging *staging);
void evidence_staging_free(EvidenceStaging *staging);
const char *evidence_staging_get_directory(const EvidenceStaging *staging);
EvidenceStagingResult *evidence_staging_prepare(EvidenceStaging *staging,
const char *source_path, GCancellable *cancellable, GError **error);
void evidence_staging_result_free(EvidenceStagingResult *result);
gboolean evidence_staging_remove(EvidenceStaging *staging,
const char *staging_path, GError **error);
gboolean evidence_staging_cleanup(EvidenceStaging *staging, GError **error);
G_END_DECLS
#endif

View file

@ -0,0 +1,14 @@
#ifndef LABFY_INVESTIGATION_EVIDENCE_STAGING_TASK_H
#define LABFY_INVESTIGATION_EVIDENCE_STAGING_TASK_H
#include "core/background_task.h"
#include "core/evidence_staging.h"
#include "core/task_manager.h"
G_BEGIN_DECLS
BackgroundTask *evidence_staging_task_start(TaskManager *manager,
EvidenceStaging *staging, const GPtrArray *source_paths,
BackgroundTaskCompletionCallback callback, gpointer callback_data,
GDestroyNotify callback_data_destroy, GError **error);
G_END_DECLS
#endif

View file

@ -0,0 +1,25 @@
#ifndef LABFY_EVIDENCE_VIDEO_PREVIEW_CONTROLLER_H
#define LABFY_EVIDENCE_VIDEO_PREVIEW_CONTROLLER_H
#include <glib.h>
G_BEGIN_DECLS
typedef struct EvidenceVideoPreviewController EvidenceVideoPreviewController;
typedef struct {
void (*pause)(gpointer media);
void (*seek_start)(gpointer media);
void (*detach)(gpointer owner);
void (*release)(gpointer media);
} EvidenceVideoPreviewActions;
EvidenceVideoPreviewController *evidence_video_preview_controller_new(
gpointer owner, const EvidenceVideoPreviewActions *actions);
void evidence_video_preview_controller_replace(
EvidenceVideoPreviewController *controller, gpointer media);
void evidence_video_preview_controller_stop(
EvidenceVideoPreviewController *controller);
void evidence_video_preview_controller_free(
EvidenceVideoPreviewController *controller);
void evidence_video_preview_controller_abandon_owner(
EvidenceVideoPreviewController *controller);
gboolean evidence_video_preview_controller_has_media(
const EvidenceVideoPreviewController *controller);
G_END_DECLS
#endif

View file

@ -2,6 +2,7 @@
#define LABFY_INVESTIGATION_PERSON_CONFIRMATION_SUMMARY_H
#include "models/evidence_record.h"
#include "models/person_evidence_selection.h"
#include <glib.h>
G_BEGIN_DECLS
@ -10,6 +11,11 @@ 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);
char *person_confirmation_summary_build_multiple(const char *designation,
const char *declared_name, const char *pseudonym,
const char *identification_status, gint confidence, const char *notes,
const GPtrArray *role_labels,
const PersonEvidenceSelection *selection);
G_END_DECLS

View file

@ -0,0 +1,24 @@
#ifndef LABFY_INVESTIGATION_PERSON_CREATION_COORDINATOR_H
#define LABFY_INVESTIGATION_PERSON_CREATION_COORDINATOR_H
#include "core/person_entity_service.h"
#include "models/person_evidence_selection.h"
#include <gio/gio.h>
G_BEGIN_DECLS
typedef struct {
char *person_identifier;
GPtrArray *evidence_identifiers;
} PersonCreationCoordinatorResult;
PersonCreationCoordinatorResult *person_creation_coordinator_execute(
Database *database, const char *investigation_root_path,
const PersonEntityInput *person,
const PersonEvidenceSelection *selection,
GCancellable *cancellable, GError **error);
void person_creation_coordinator_result_free(
PersonCreationCoordinatorResult *result);
G_END_DECLS
#endif

View file

@ -0,0 +1,20 @@
#ifndef LABFY_INVESTIGATION_PERSON_CREATION_TASK_H
#define LABFY_INVESTIGATION_PERSON_CREATION_TASK_H
#include "core/background_task.h"
#include "core/person_creation_coordinator.h"
#include "core/task_manager.h"
G_BEGIN_DECLS
typedef struct PersonCreationTaskRequest PersonCreationTaskRequest;
PersonCreationTaskRequest *person_creation_task_request_new(
const char *database_path, const char *investigation_root_path,
const PersonEntityInput *person,
const PersonEvidenceSelection *selection);
void person_creation_task_request_free(PersonCreationTaskRequest *request);
BackgroundTask *person_creation_task_start(TaskManager *manager,
const PersonCreationTaskRequest *request,
BackgroundTaskCompletionCallback callback, gpointer callback_data,
GDestroyNotify callback_data_destroy, GError **error);
G_END_DECLS
#endif

View file

@ -186,5 +186,7 @@ gboolean evidence_dao_count(
guint64 *out_count,
GError **error
);
char *evidence_dao_find_identifier_by_sha256(EvidenceDao *evidence_dao,
const char *sha256, GError **error);
#endif

View file

@ -19,5 +19,9 @@ const EvidenceRecord *evidence_selection_model_get_selected(
const EvidenceSelectionModel *model);
GPtrArray *evidence_selection_model_list_type_codes(
const EvidenceSelectionModel *model);
const EvidenceRecord *evidence_selection_model_find_by_sha256(
const EvidenceSelectionModel *model, const char *sha256);
const EvidenceRecord *evidence_selection_model_find_by_identifier(
const EvidenceSelectionModel *model, const char *identifier);
G_END_DECLS
#endif

View file

@ -0,0 +1,87 @@
#ifndef LABFY_INVESTIGATION_PERSON_EVIDENCE_SELECTION_H
#define LABFY_INVESTIGATION_PERSON_EVIDENCE_SELECTION_H
#include "models/evidence_record.h"
#include <glib.h>
G_BEGIN_DECLS
typedef enum {
PERSON_EVIDENCE_ORIGIN_EXISTING,
PERSON_EVIDENCE_ORIGIN_STAGED,
PERSON_EVIDENCE_ORIGIN_IMPORTED
} PersonEvidenceOrigin;
typedef enum {
PERSON_EVIDENCE_STATE_PREPARING,
PERSON_EVIDENCE_STATE_READY,
PERSON_EVIDENCE_STATE_DUPLICATE,
PERSON_EVIDENCE_STATE_INVALID,
PERSON_EVIDENCE_STATE_ERROR,
PERSON_EVIDENCE_STATE_CANCELLED
} PersonEvidenceState;
typedef struct PersonEvidenceSelection PersonEvidenceSelection;
typedef struct PersonEvidenceSelectionItem PersonEvidenceSelectionItem;
PersonEvidenceSelection *person_evidence_selection_new(void);
PersonEvidenceSelection *person_evidence_selection_copy(
const PersonEvidenceSelection *selection);
void person_evidence_selection_free(PersonEvidenceSelection *selection);
guint person_evidence_selection_get_count(
const PersonEvidenceSelection *selection);
const PersonEvidenceSelectionItem *person_evidence_selection_get(
const PersonEvidenceSelection *selection, guint index);
const PersonEvidenceSelectionItem *person_evidence_selection_get_active(
const PersonEvidenceSelection *selection);
gboolean person_evidence_selection_set_active(
PersonEvidenceSelection *selection, const char *item_identifier);
gboolean person_evidence_selection_add_existing(
PersonEvidenceSelection *selection, const EvidenceRecord *record,
GError **error);
gboolean person_evidence_selection_add_staged(
PersonEvidenceSelection *selection, const char *source_path,
const char *staging_path, const char *original_name,
const char *mime_type, const char *type_identifier, guint64 size_bytes,
const char *sha256, const char *description, const char *prepared_at,
GError **error);
gboolean person_evidence_selection_remove(PersonEvidenceSelection *selection,
const char *item_identifier);
gboolean person_evidence_selection_set_type(PersonEvidenceSelection *selection,
const char *item_identifier, const char *type_identifier);
gboolean person_evidence_selection_is_confirmable(
const PersonEvidenceSelection *selection);
const char *person_evidence_selection_find_existing_by_sha256(
const PersonEvidenceSelection *selection, const char *sha256);
const char *person_evidence_selection_item_get_identifier(
const PersonEvidenceSelectionItem *item);
PersonEvidenceOrigin person_evidence_selection_item_get_origin(
const PersonEvidenceSelectionItem *item);
PersonEvidenceState person_evidence_selection_item_get_state(
const PersonEvidenceSelectionItem *item);
const EvidenceRecord *person_evidence_selection_item_get_record(
const PersonEvidenceSelectionItem *item);
const char *person_evidence_selection_item_get_evidence_identifier(
const PersonEvidenceSelectionItem *item);
const char *person_evidence_selection_item_get_source_path(
const PersonEvidenceSelectionItem *item);
const char *person_evidence_selection_item_get_staging_path(
const PersonEvidenceSelectionItem *item);
const char *person_evidence_selection_item_get_original_name(
const PersonEvidenceSelectionItem *item);
const char *person_evidence_selection_item_get_mime_type(
const PersonEvidenceSelectionItem *item);
const char *person_evidence_selection_item_get_type_identifier(
const PersonEvidenceSelectionItem *item);
const char *person_evidence_selection_item_get_sha256(
const PersonEvidenceSelectionItem *item);
const char *person_evidence_selection_item_get_description(
const PersonEvidenceSelectionItem *item);
const char *person_evidence_selection_item_get_prepared_at(
const PersonEvidenceSelectionItem *item);
guint64 person_evidence_selection_item_get_size_bytes(
const PersonEvidenceSelectionItem *item);
G_END_DECLS
#endif

View file

@ -5,7 +5,9 @@
#ifndef LABFY_INVESTIGATION_CREATE_PERSON_DIALOG_H
#define LABFY_INVESTIGATION_CREATE_PERSON_DIALOG_H
#include "core/person_entity_service.h"
#include "core/evidence_staging.h"
#include "models/evidence_record.h"
#include "models/person_evidence_selection.h"
#include "core/task_manager.h"
#include <gtk/gtk.h>
G_BEGIN_DECLS
@ -33,5 +35,9 @@ void create_person_dialog_result_free(CreatePersonDialogResult *result);
/** @brief Retourne les données empruntées du résultat. */
const PersonEntityInput *create_person_dialog_result_get_input(
const CreatePersonDialogResult *result);
const PersonEvidenceSelection *create_person_dialog_result_get_evidence_selection(
const CreatePersonDialogResult *result);
EvidenceStaging *create_person_dialog_result_steal_staging(
CreatePersonDialogResult *result);
G_END_DECLS
#endif

View file

@ -509,6 +509,8 @@ void main_window_set_eml_analysis_available(
/** @brief Transmet au workspace un chemin absolu validé pour l'aperçu. */
void main_window_set_evidence_preview(MainWindow *main_window,
const char *file_path, const char *display_name);
void main_window_show_evidence_preview(MainWindow *main_window,
const char *investigation_root, const EvidenceRecord *record);
/**

View file

@ -0,0 +1,30 @@
#ifndef LABFY_INVESTIGATION_EVIDENCE_PREVIEW_WIDGET_H
#define LABFY_INVESTIGATION_EVIDENCE_PREVIEW_WIDGET_H
#include "core/evidence_preview.h"
#include "core/task_manager.h"
#include <gtk/gtk.h>
G_BEGIN_DECLS
typedef struct EvidencePreviewWidget EvidencePreviewWidget;
typedef gboolean (*EvidencePreviewWidgetSessionCheck)(gpointer user_data);
EvidencePreviewWidget *evidence_preview_widget_new(
TaskManager *task_manager,
EvidencePreviewWidgetSessionCheck session_check,
gpointer session_data);
GtkWidget *evidence_preview_widget_get_widget(
const EvidencePreviewWidget *widget);
void evidence_preview_widget_show(EvidencePreviewWidget *widget,
const EvidencePreviewRequest *request, const char *metadata);
void evidence_preview_widget_clear(EvidencePreviewWidget *widget);
void evidence_preview_widget_cancel(EvidencePreviewWidget *widget);
void evidence_preview_widget_free(EvidencePreviewWidget *widget);
const char *evidence_preview_widget_get_state(
const EvidencePreviewWidget *widget);
char *evidence_preview_widget_dup_text(
const EvidencePreviewWidget *widget);
G_END_DECLS
#endif

View file

@ -11,6 +11,7 @@
#include "models/evidence_observation.h"
#include "models/entity_record.h"
#include "models/osint_action_catalog.h"
#include "core/task_manager.h"
#include <gtk/gtk.h>
@ -162,7 +163,7 @@ typedef void (*WorkspaceObservationRemoveCallback)(
*
* @return Une nouvelle zone de travail, ou NULL en cas d'échec.
*/
Workspace *workspace_new(void);
Workspace *workspace_new(TaskManager *task_manager);
/**
* @brief Retourne le widget GTK racine de la zone de travail.
@ -223,6 +224,8 @@ void workspace_set_eml_analysis_available(
*/
void workspace_set_evidence_preview(Workspace *workspace,
const char *file_path, const char *display_name);
void workspace_show_evidence_preview(Workspace *workspace,
const char *investigation_root, const EvidenceRecord *record);
/**
* @brief Sélectionne une entité dans le graphe affiché.

View file

@ -56,6 +56,8 @@
#include "core/relation_service.h"
#include "core/social_account_service.h"
#include "core/person_entity_service.h"
#include "core/person_creation_coordinator.h"
#include "core/person_creation_task.h"
#include "core/person_creation_guard.h"
#include "core/eml_processing.h"
#include "core/eml_integration.h"
@ -157,6 +159,14 @@ typedef struct
Application *application;
PersonCreationGuard *guard;
} ApplicationPersonDialogContext;
typedef struct
{
Application *application;
guint64 generation;
char *root_path;
char *database_path;
EvidenceStaging *staging;
} ApplicationPersonCreationTaskContext;
static void application_person_dialog_context_free(
ApplicationPersonDialogContext *context)
@ -165,6 +175,15 @@ static void application_person_dialog_context_free(
person_creation_guard_free(context->guard);
g_free(context);
}
static void application_person_creation_task_context_free(gpointer data)
{
ApplicationPersonCreationTaskContext *context = data;
if (context == NULL) return;
g_free(context->root_path);
g_free(context->database_path);
evidence_staging_free(context->staging);
g_free(context);
}
static void application_on_evidence_selected(
const char *evidence_identifier, gpointer user_data);
@ -2736,6 +2755,7 @@ static gboolean application_install_session(
application->graph_viewport_save_source_id = 0U;
}
application_save_graph_viewport(application);
task_manager_cancel_all(application->task_manager);
investigation_session_close(
application->session
);
@ -5394,6 +5414,32 @@ static void application_on_add_social_account_requested(gpointer user_data)
}
/** @brief Persiste une personne validée puis recharge le graphe. */
static void application_on_person_creation_task_completed(
BackgroundTask *task, gpointer user_data)
{
ApplicationPersonCreationTaskContext *context = user_data;
Application *application = context != NULL ? context->application : NULL;
const InvestigationProject *project = application != NULL &&
application->session != NULL
? investigation_session_get_project(application->session) : NULL;
gboolean session_matches = project != NULL &&
application->session_generation == context->generation &&
g_strcmp0(investigation_project_get_root_path(project),
context->root_path) == 0 &&
g_strcmp0(investigation_project_get_database_path(project),
context->database_path) == 0;
if (!session_matches) return;
if (background_task_get_state(task) != BACKGROUND_TASK_STATE_COMPLETED) {
GError *error = background_task_dup_error(task);
application_present_error(application, "Personne non créée",
error != NULL ? error->message : "Lécriture a échoué.");
g_clear_error(&error);
return;
}
main_window_set_status(application->main_window,
"Personne et preuves ajoutées. Actualisation du graphe…");
application_start_graph_loading(application, context->database_path);
}
static void application_on_person_completed(
CreatePersonDialogResult *result, gpointer user_data)
{
@ -5402,7 +5448,9 @@ static void application_on_person_completed(
const PersonEntityInput *input = NULL;
const InvestigationProject *project = NULL;
GError *error = NULL;
char *identifier = NULL;
PersonCreationTaskRequest *request = NULL;
BackgroundTask *task = NULL;
ApplicationPersonCreationTaskContext *task_context = NULL;
const char *active_root = NULL, *active_database = NULL;
if (result == NULL) {
return;
@ -5416,20 +5464,29 @@ static void application_on_person_completed(
if (!person_creation_guard_matches(context->guard, TRUE,
application->session_generation, active_root, active_database))
goto changed_session;
if (!person_entity_service_create(
investigation_session_get_database(application->session),
input, &identifier, &error))
request = person_creation_task_request_new(active_database, active_root,
input, create_person_dialog_result_get_evidence_selection(result));
task_context = g_new0(ApplicationPersonCreationTaskContext, 1);
task_context->application = application;
task_context->generation = application->session_generation;
task_context->root_path = g_strdup(active_root);
task_context->database_path = g_strdup(active_database);
task_context->staging =
create_person_dialog_result_steal_staging(result);
task = person_creation_task_start(application->task_manager, request,
application_on_person_creation_task_completed, task_context,
application_person_creation_task_context_free, &error);
if (task == NULL) {
application_person_creation_task_context_free(task_context);
application_present_error(application, "Personne non créée",
error != NULL ? error->message : "L'écriture a échoué.");
else
{
project = investigation_session_get_project(application->session);
} else {
main_window_set_status(application->main_window,
"Personne ajoutée. Actualisation du graphe…");
application_start_graph_loading(application,
investigation_project_get_database_path(project));
"Création et import des preuves en cours…");
background_task_unref(task);
}
g_clear_error(&error); g_free(identifier);
person_creation_task_request_free(request);
g_clear_error(&error);
create_person_dialog_result_free(result);
return;
changed_session:
@ -5577,10 +5634,6 @@ static void application_on_evidence_selected(
const InvestigationProject *project = NULL;
const char *investigation_root = NULL;
const char *relative_path = NULL;
char *canonical_root = NULL;
char *candidate_path = NULL;
char *canonical_path = NULL;
char *status_message =
NULL;
@ -5775,30 +5828,17 @@ static void application_on_evidence_selected(
project = investigation_session_get_project(application->session);
investigation_root = project != NULL
? investigation_project_get_root_path(project) : NULL;
relative_path = evidence_record_get_relative_path(evidence_record);
gboolean eml_analysis_available = FALSE;
if (investigation_root != NULL && relative_path != NULL)
{
canonical_root = g_canonicalize_filename(investigation_root, NULL);
candidate_path = g_build_filename(investigation_root,
relative_path, NULL);
canonical_path = g_canonicalize_filename(candidate_path, NULL);
if (canonical_root != NULL && canonical_path != NULL &&
g_str_has_prefix(canonical_path, canonical_root) &&
canonical_path[strlen(canonical_root)] == G_DIR_SEPARATOR &&
g_file_test(canonical_path, G_FILE_TEST_IS_REGULAR))
{
const char *selected_name =
evidence_record_get_original_name(evidence_record);
char *lower_name = selected_name != NULL
? g_ascii_strdown(selected_name, -1) : NULL;
eml_analysis_available = lower_name != NULL &&
g_str_has_suffix(lower_name, ".eml");
g_free(lower_name);
main_window_set_evidence_preview(application->main_window,
canonical_path,
evidence_record_get_original_name(evidence_record));
}
if (investigation_root != NULL) {
const char *selected_name =
evidence_record_get_original_name(evidence_record);
char *lower_name = selected_name != NULL
? g_ascii_strdown(selected_name, -1) : NULL;
eml_analysis_available = lower_name != NULL &&
g_str_has_suffix(lower_name, ".eml");
g_free(lower_name);
main_window_show_evidence_preview(application->main_window,
investigation_root, evidence_record);
}
main_window_set_eml_analysis_available(
application->main_window, eml_analysis_available);
@ -5825,9 +5865,6 @@ static void application_on_evidence_selected(
g_free(
status_message
);
g_free(canonical_path);
g_free(candidate_path);
g_free(canonical_root);
evidence_record_free(
evidence_record

View file

@ -12,6 +12,7 @@
#define EML_MIME_MAX_COLLISIONS 10000U
#define EML_MIME_IO_BLOCK_SIZE 65536U
#define EML_MIME_PREVIEW_BODY_MAX (1024U * 1024U)
typedef struct
{
@ -26,6 +27,8 @@ typedef struct
EmlMimeResult *result;
guint part_count;
gsize total_decoded_size;
gboolean preview_only;
guint body_rank;
} EmlMimeContext;
static gboolean eml_mime_check_cancelled(
@ -65,6 +68,7 @@ void eml_mime_result_free(EmlMimeResult *result)
return;
g_clear_pointer(&result->attachments, g_ptr_array_unref);
g_clear_pointer(&result->warnings, g_ptr_array_unref);
g_free(result->body_text);
g_free(result);
}
@ -1201,7 +1205,10 @@ static gboolean eml_mime_extract_leaf(
g_ascii_strcasecmp(disposition, "attachment") == 0 ||
g_ascii_strcasecmp(disposition, "inline") == 0;
if (!relevant)
gboolean body_candidate = !relevant &&
(g_str_equal(content_type, "text/plain") ||
g_str_equal(content_type, "text/html"));
if (!relevant && !body_candidate)
goto cleanup;
if (disposition_malformed)
eml_mime_warn(
@ -1239,6 +1246,36 @@ static gboolean eml_mime_extract_leaf(
gsize decoded_length = 0;
gconstpointer decoded_data = g_bytes_get_data(decoded, &decoded_length);
if (body_candidate)
{
guint rank = g_str_equal(content_type, "text/plain") ? 2U : 1U;
if (rank >= context->body_rank)
{
gsize kept = MIN(decoded_length,
(gsize) EML_MIME_PREVIEW_BODY_MAX);
char *valid = g_utf8_make_valid(decoded_data, (gssize) kept);
if (rank == 1U)
{
GRegex *active = g_regex_new(
"(?is)<(script|style)\\b[^>]*>.*?</\\1\\s*>",
0, 0, NULL);
char *without_active = g_regex_replace(active, valid, -1,
0, " ", 0, NULL);
GRegex *tags = g_regex_new("(?s)<[^>]+>", 0, 0, NULL);
char *plain = g_regex_replace(tags, without_active, -1,
0, " ", 0, NULL);
g_regex_unref(active); g_regex_unref(tags);
g_free(without_active); g_free(valid); valid = plain;
}
g_free(context->result->body_text);
context->result->body_text = valid;
context->result->body_from_html = rank == 1U;
context->result->body_truncated =
decoded_length > EML_MIME_PREVIEW_BODY_MAX;
context->body_rank = rank;
}
goto cleanup;
}
if (context->total_decoded_size >
EML_MIME_MAX_TOTAL_DECODED_SIZE - decoded_length)
{
@ -1262,6 +1299,34 @@ static gboolean eml_mime_extract_leaf(
? raw_filename
: decoded_name;
char *sanitized = eml_mime_sanitize_filename(decoded_name);
if (context->preview_only)
{
EmlAttachment *attachment = g_new0(EmlAttachment, 1);
attachment->part_index = g_strdup(part_index);
attachment->declared_filename = g_strdup(declared);
attachment->decoded_filename = g_strdup(decoded_name);
attachment->sanitized_filename = g_strdup(sanitized);
attachment->content_type = g_strdup(content_type);
attachment->detected_mime = g_strdup(content_type);
attachment->content_id = g_strdup(content_id);
attachment->normalized_content_id =
eml_mime_normalize_content_id(content_id);
attachment->content_disposition = g_strdup(raw_disposition);
attachment->normalized_disposition = disposition[0] != '\0'
? g_ascii_strdown(disposition, -1) : NULL;
attachment->transfer_encoding = g_strdup(
encoding != NULL ? encoding : "7bit");
attachment->is_inline =
g_ascii_strcasecmp(disposition, "inline") == 0;
attachment->is_attachment =
g_ascii_strcasecmp(disposition, "attachment") == 0;
attachment->encoded_size = body_length;
attachment->decoded_size = decoded_length;
context->total_decoded_size += decoded_length;
g_ptr_array_add(context->result->attachments, attachment);
g_free(sanitized); g_free(synthetic);
goto cleanup;
}
char *unique = eml_mime_unique_name(
context->target_dir,
sanitized,
@ -1726,3 +1791,46 @@ EmlMimeResult *eml_mime_extract_attachments(
error
);
}
EmlMimeResult *eml_mime_build_preview(const char *eml_path,
GCancellable *cancellable, GError **error)
{
g_return_val_if_fail(error == NULL || *error == NULL, NULL);
if (eml_path == NULL || eml_path[0] == '\0')
{
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
"Le chemin de l'EML est invalide.");
return NULL;
}
if (eml_mime_check_cancelled(cancellable, error))
return NULL;
GMappedFile *mapped = g_mapped_file_new(eml_path, FALSE, error);
if (mapped == NULL)
return NULL;
gsize size = g_mapped_file_get_length(mapped);
if (size == 0 || size > EML_MIME_MAX_FILE_SIZE)
{
g_mapped_file_unref(mapped);
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
"Le fichier EML est vide ou dépasse la limite autorisée.");
return NULL;
}
EmlMimeResult *result = g_new0(EmlMimeResult, 1);
result->attachments = g_ptr_array_new_with_free_func(
(GDestroyNotify) eml_attachment_free);
result->warnings = g_ptr_array_new_with_free_func(g_free);
EmlMimeContext context = {
.cancellable = cancellable,
.result = result,
.preview_only = TRUE
};
if (!eml_mime_parse_entity(&context, g_mapped_file_get_contents(mapped),
size, "1", 1, error))
{
g_mapped_file_unref(mapped);
eml_mime_result_free(result);
return NULL;
}
g_mapped_file_unref(mapped);
return result;
}

View file

@ -1,35 +1,117 @@
#include "core/evidence_preview.h"
#include "core/evidence_integrity_verifier.h"
#include "core/eml_analyzer.h"
#include "core/eml_mime_extractor.h"
#include <cairo.h>
#include <libheif/heif.h>
#include <poppler.h>
#include <stdio.h>
#include <jpeglib.h>
#include <setjmp.h>
#include <string.h>
typedef enum {
FORMAT_UNKNOWN, FORMAT_PNG, FORMAT_JPEG, FORMAT_HEIF, FORMAT_HEIC,
FORMAT_MP4, FORMAT_MOV, FORMAT_PDF, FORMAT_EML, FORMAT_TEXT
} PreviewFormat;
typedef struct { struct jpeg_error_mgr base; jmp_buf jump; } PreviewJpegError;
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)
{ longjmp(((PreviewJpegError *) info->err)->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 cancelled(GCancellable *cancellable, GError **error)
{ return cancellable != NULL &&
g_cancellable_set_error_if_cancelled(cancellable, error); }
static gboolean brand_is(const guint8 *data, gsize size, const char *brand)
{
PreviewJpegError *error = (PreviewJpegError *) info->err;
longjmp(error->jump, 1);
if (size < 12 || memcmp(data + 4, "ftyp", 4) != 0) return FALSE;
for (gsize offset = 8; offset + 4 <= size; offset += 4)
if (memcmp(data + offset, brand, 4) == 0) return TRUE;
return FALSE;
}
static cairo_status_t write_png(void *closure,
const unsigned char *data, unsigned int length)
static gboolean looks_like_eml(const guint8 *data, gsize size)
{
g_byte_array_append(closure, data, length);
return CAIRO_STATUS_SUCCESS;
char *text = g_strndup((const char *) data, MIN(size, 65536U));
gboolean result = (g_str_has_prefix(text, "From:") ||
strstr(text, "\nFrom:") != NULL) &&
(strstr(text, "\nSubject:") != NULL ||
strstr(text, "\nDate:") != NULL) &&
(strstr(text, "\r\n\r\n") != NULL || strstr(text, "\n\n") != NULL);
g_free(text); return result;
}
static gboolean looks_textual(const guint8 *data, gsize size)
{
if (memchr(data, '\0', size) != NULL) return FALSE;
gsize invalid = 0;
for (gsize i = 0; i < size; i++)
if (data[i] < 0x09 || (data[i] > 0x0d && data[i] < 0x20)) invalid++;
return size == 0 || invalid * 100U <= size;
}
static PreviewFormat detect_format(const guint8 *data, gsize size,
const char *path, const char *historical_mime)
{
if (size >= 8 && memcmp(data, "\x89PNG\r\n\x1a\n", 8) == 0)
return FORMAT_PNG;
if (size >= 3 && data[0] == 0xff && data[1] == 0xd8 && data[2] == 0xff)
return FORMAT_JPEG;
if (size >= 5 && memcmp(data, "%PDF-", 5) == 0) return FORMAT_PDF;
if (brand_is(data, size, "heic") || brand_is(data, size, "heix") ||
brand_is(data, size, "hevc") || brand_is(data, size, "hevx"))
return FORMAT_HEIC;
if (brand_is(data, size, "mif1") || brand_is(data, size, "msf1") ||
brand_is(data, size, "heif")) return FORMAT_HEIF;
if (brand_is(data, size, "qt ")) return FORMAT_MOV;
if (brand_is(data, size, "isom") || brand_is(data, size, "mp41") ||
brand_is(data, size, "mp42") || brand_is(data, size, "avc1"))
return FORMAT_MP4;
if (looks_like_eml(data, size)) return FORMAT_EML;
if (looks_textual(data, size)) {
const char *suffix = strrchr(path, '.');
gboolean misleading = suffix != NULL &&
(g_ascii_strcasecmp(suffix, ".png") == 0 ||
g_ascii_strcasecmp(suffix, ".jpg") == 0 ||
g_ascii_strcasecmp(suffix, ".jpeg") == 0 ||
g_ascii_strcasecmp(suffix, ".heic") == 0 ||
g_ascii_strcasecmp(suffix, ".heif") == 0 ||
g_ascii_strcasecmp(suffix, ".mp4") == 0 ||
g_ascii_strcasecmp(suffix, ".mov") == 0 ||
g_ascii_strcasecmp(suffix, ".pdf") == 0 ||
g_ascii_strcasecmp(suffix, ".eml") == 0);
if (misleading || (historical_mime != NULL &&
!g_str_has_prefix(historical_mime, "text/"))) return FORMAT_UNKNOWN;
char *content_type = g_content_type_guess(path, data, size, NULL);
char *mime = content_type != NULL
? g_content_type_get_mime_type(content_type) : NULL;
gboolean indicated = mime != NULL && g_str_has_prefix(mime, "text/");
indicated = indicated || (historical_mime != NULL &&
g_str_has_prefix(historical_mime, "text/"));
g_free(mime); g_free(content_type);
if (indicated || size == 0) return FORMAT_TEXT;
}
return FORMAT_UNKNOWN;
}
static EvidencePreviewResult *result_new(const EvidencePreviewRequest *request,
const char *path, guint64 size)
{
EvidencePreviewResult *result = g_new0(EvidencePreviewResult, 1);
result->kind = EVIDENCE_PREVIEW_KIND_UNSUPPORTED;
result->evidence_identifier = g_strdup(request->evidence_identifier);
result->request_generation = request->request_generation;
result->display_name = g_path_get_basename(path);
result->controlled_path = g_strdup(path);
result->size_bytes = size;
result->integrity_valid = TRUE;
return result;
}
static gboolean surface_to_result(cairo_surface_t *source, gint source_width,
gint source_height, const EvidencePreviewRequest *request,
EvidencePreviewResult **out_result, GError **error)
gint source_height, EvidencePreviewResult *result, GError **error)
{
double scale = MIN(1.0, MIN(
(double) EVIDENCE_PREVIEW_MAX_EDGE / source_width,
(double) EVIDENCE_PREVIEW_MAX_EDGE / source_height));
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(
@ -37,177 +119,356 @@ static gboolean surface_to_result(cairo_surface_t *source, gint source_width,
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_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;
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);
result->width = width; result->height = height;
result->png_bytes = g_byte_array_free_to_bytes(bytes);
result->preview_available = TRUE;
return TRUE;
}
static gboolean valid_dimensions(gint width, gint height, GError **error)
{
if (!evidence_preview_dimensions_are_safe(width, height)) {
g_set_error_literal(error, preview_error(), 5,
"Les dimensions dépassent les limites sûres.");
return FALSE;
}
return TRUE;
}
gboolean evidence_preview_dimensions_are_safe(gint width, gint height)
{
return width > 0 && height > 0 &&
width <= EVIDENCE_PREVIEW_MAX_DIMENSION &&
height <= EVIDENCE_PREVIEW_MAX_DIMENSION &&
(guint64) width * (guint64) height <= EVIDENCE_PREVIEW_MAX_PIXELS;
}
static gboolean load_png(const char *path, EvidencePreviewResult *result,
GError **error)
{
cairo_surface_t *surface = cairo_image_surface_create_from_png(path);
gint width = cairo_image_surface_get_width(surface);
gint height = cairo_image_surface_get_height(surface);
if (cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS ||
!valid_dimensions(width, height, error)) {
if (error != NULL && *error == NULL) g_set_error_literal(error,
preview_error(), 4, "Le fichier PNG est illisible ou corrompu.");
cairo_surface_destroy(surface); return FALSE;
}
gboolean ok = surface_to_result(surface, width, height, result, error);
cairo_surface_destroy(surface); return ok;
}
static gboolean load_jpeg(const char *path, EvidencePreviewResult *result,
GCancellable *cancellable, GError **error)
{
FILE *file = fopen(path, "rb"); struct jpeg_decompress_struct jpeg = {0};
PreviewJpegError jpeg_error; cairo_surface_t *surface = NULL;
if (file == NULL) goto invalid;
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(file);
if (surface != NULL) cairo_surface_destroy(surface);
goto invalid;
}
jpeg_create_decompress(&jpeg); jpeg_stdio_src(&jpeg, file);
jpeg_read_header(&jpeg, TRUE);
gint width = (gint) jpeg.image_width, height = (gint) jpeg.image_height;
if (!valid_dimensions(width, height, error)) {
jpeg_destroy_decompress(&jpeg); fclose(file); return FALSE;
}
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);
guint32 *destination = (guint32 *) (pixels +
(jpeg.output_scanline - 1) * 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(file);
if (cancelled(cancellable, error)) {
cairo_surface_destroy(surface); return FALSE;
}
gboolean ok = surface_to_result(surface, width, height, result, error);
cairo_surface_destroy(surface); return ok;
invalid:
g_set_error_literal(error, preview_error(), 4,
"Le fichier JPEG est illisible ou corrompu."); return FALSE;
}
static gboolean load_heif(const char *path, EvidencePreviewResult *result,
GError **error)
{
struct heif_context *context = heif_context_alloc();
struct heif_image_handle *handle = NULL; struct heif_image *image = NULL;
struct heif_error heif_error = heif_context_read_from_file(
context, path, NULL);
if (heif_error.code == heif_error_Ok)
heif_error = heif_context_get_primary_image_handle(context, &handle);
gint width = handle != NULL ? heif_image_handle_get_width(handle) : 0;
gint height = handle != NULL ? heif_image_handle_get_height(handle) : 0;
result->item_count = (guint)
MAX(0, heif_context_get_number_of_top_level_images(context));
if (heif_error.code != heif_error_Ok ||
!valid_dimensions(width, height, error)) goto failed;
heif_error = heif_decode_image(handle, &image, heif_colorspace_RGB,
heif_chroma_interleaved_RGBA, NULL);
if (heif_error.code != heif_error_Ok) goto failed;
gint stride = 0;
const guint8 *plane = heif_image_get_plane_readonly(image,
heif_channel_interleaved, &stride);
cairo_surface_t *surface = cairo_image_surface_create(
CAIRO_FORMAT_ARGB32, width, height);
guint8 *target = cairo_image_surface_get_data(surface);
gint target_stride = cairo_image_surface_get_stride(surface);
for (gint y = 0; y < height; y++) {
guint32 *row = (guint32 *) (target + y * target_stride);
for (gint x = 0; x < width; x++) {
const guint8 *pixel = plane + y * stride + x * 4;
row[x] = ((guint32) pixel[3] << 24) |
((guint32) pixel[0] << 16) |
((guint32) pixel[1] << 8) | pixel[2];
}
}
cairo_surface_mark_dirty(surface);
gboolean ok = surface_to_result(surface, width, height, result, error);
cairo_surface_destroy(surface); heif_image_release(image);
heif_image_handle_release(handle); heif_context_free(context); return ok;
failed:
if (error != NULL && *error == NULL) g_set_error(error, preview_error(), 6,
"HEIC/HEIF illisible : %s", heif_error.message);
if (image != NULL) heif_image_release(image);
if (handle != NULL) heif_image_handle_release(handle);
heif_context_free(context); return FALSE;
}
static gboolean load_pdf(const char *path, EvidencePreviewResult *result,
GError **error)
{
char *uri = g_filename_to_uri(path, NULL, error);
if (uri == NULL) return FALSE;
PopplerDocument *document = poppler_document_new_from_file(uri, NULL, error);
g_free(uri); if (document == NULL) return FALSE;
gint pages = poppler_document_get_n_pages(document);
if (pages < 1) { g_object_unref(document); g_set_error_literal(error,
preview_error(), 7, "Le PDF ne contient aucune page."); return FALSE; }
PopplerPage *page = poppler_document_get_page(document, 0);
double page_width = 0, page_height = 0;
poppler_page_get_size(page, &page_width, &page_height);
if (!valid_dimensions((gint) page_width, (gint) page_height, error)) {
g_object_unref(page); g_object_unref(document); return FALSE;
}
double scale = MIN(1.0, MIN(EVIDENCE_PREVIEW_MAX_EDGE / page_width,
EVIDENCE_PREVIEW_MAX_EDGE / page_height));
gint width = MAX(1, (gint) (page_width * scale));
gint height = MAX(1, (gint) (page_height * scale));
cairo_surface_t *surface = cairo_image_surface_create(
CAIRO_FORMAT_ARGB32, width, height);
cairo_t *cr = cairo_create(surface); cairo_set_source_rgb(cr, 1, 1, 1);
cairo_paint(cr); cairo_scale(cr, scale, scale); poppler_page_render(page, cr);
cairo_destroy(cr); result->item_count = (guint) pages;
gboolean ok = surface_to_result(surface, width, height, result, error);
cairo_surface_destroy(surface); g_object_unref(page);
g_object_unref(document); return ok;
}
static char *header_line(EmlAnalysis *analysis, const char *name,
const char *label)
{
const char *value = eml_analysis_get_first_header(analysis, name);
return g_strdup_printf("%s : %s\n", label,
value != NULL ? value : "Non renseigné");
}
static gboolean load_email(const char *path, const guint8 *data, gsize size,
EvidencePreviewResult *result, GError **error)
{
EmlAnalysis *analysis = eml_analyzer_analyze_file(path, error);
if (analysis == NULL) return FALSE;
EmlMimeResult *mime = eml_mime_build_preview(path, NULL, error);
if (mime == NULL) { eml_analysis_free(analysis); return FALSE; }
GString *text = g_string_new("APERÇU EML — contenu passif\n\n");
static const struct { const char *name; const char *label; } headers[] = {
{"from","From"}, {"to","To"}, {"cc","Cc"}, {"date","Date"},
{"subject","Subject"}, {"message-id","Message-ID"}};
for (guint i = 0; i < G_N_ELEMENTS(headers); i++) {
char *line = header_line(analysis, headers[i].name, headers[i].label);
g_string_append(text, line); g_free(line);
}
(void) data; (void) size;
g_string_append(text, "\nCorps sélectionné :\n");
g_string_append(text, mime->body_text != NULL
? mime->body_text : "Corps absent.");
g_string_append_printf(text, "\n\nPièces jointes (%u) :\n",
mime->attachments->len);
for (guint i = 0; i < mime->attachments->len; i++) {
EmlAttachment *attachment = g_ptr_array_index(mime->attachments, i);
g_string_append_printf(text,
"• %s — %s — %" G_GSIZE_FORMAT " octets — %s%s%s\n",
attachment->decoded_filename,
attachment->content_type,
attachment->decoded_size,
attachment->normalized_disposition != NULL
? attachment->normalized_disposition : "sans disposition",
attachment->normalized_content_id != NULL ? " — Content-ID: " : "",
attachment->normalized_content_id != NULL
? attachment->normalized_content_id : "");
}
if (mime->warnings->len > 0) {
g_string_append(text, "\nAvertissements :\n");
for (guint i = 0; i < mime->warnings->len; i++)
g_string_append_printf(text, "• %s\n",
(const char *) g_ptr_array_index(mime->warnings, i));
}
result->item_count = mime->attachments->len;
result->truncated = mime->body_truncated;
result->text = g_string_free(text, FALSE);
result->preview_available = TRUE;
eml_mime_result_free(mime); eml_analysis_free(analysis); 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)
const char *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;
if (root == NULL || !g_uuid_string_is_valid(identifier) ||
relative_path == NULL || sha256 == NULL) return NULL;
EvidencePreviewRequest *request = g_new0(EvidencePreviewRequest, 1);
request->investigation_root_path = g_strdup(root);
request->evidence_identifier = g_strdup(identifier);
request->relative_path = g_strdup(relative_path);
request->expected_sha256 = g_strdup(sha256);
request->mime_type = g_strdup(mime_type);
request->request_generation = generation; return request;
}
void evidence_preview_request_free(EvidencePreviewRequest *r)
void evidence_preview_request_free(EvidencePreviewRequest *request)
{
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);
if (request == NULL) return;
g_free(request->investigation_root_path); g_free(request->evidence_identifier);
g_free(request->relative_path); g_free(request->expected_sha256);
g_free(request->mime_type); g_free(request);
}
void evidence_preview_result_free(EvidencePreviewResult *r)
void evidence_preview_result_free(EvidencePreviewResult *result)
{
if (r == NULL) return;
g_free(r->evidence_identifier);
g_clear_pointer(&r->png_bytes, g_bytes_unref);
g_free(r);
if (result == NULL) return;
g_free(result->evidence_identifier); g_free(result->effective_mime_type);
g_free(result->effective_format); g_free(result->display_name);
g_free(result->controlled_path); g_free(result->text);
g_free(result->message); g_clear_pointer(&result->png_bytes, g_bytes_unref);
g_free(result);
}
gboolean evidence_preview_result_matches(const EvidencePreviewResult *r,
gboolean evidence_preview_result_matches(const EvidencePreviewResult *result,
const char *identifier, guint64 generation)
{
return r != NULL && r->request_generation == generation &&
g_strcmp0(r->evidence_identifier, identifier) == 0;
}
{ return result != NULL && result->request_generation == generation &&
g_strcmp0(result->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 (request == NULL) { g_set_error_literal(error, preview_error(), 1,
"Aperçu indisponible."); return NULL; }
EvidenceIntegrityVerificationResult *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) {
evidence_integrity_verification_result_free(verification);
g_set_error_literal(error, preview_error(), 2,
"Aperçu refusé : l'intégrité de la preuve est invalide.");
goto cleanup;
return NULL;
}
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,
guint64 size = evidence_integrity_verification_result_get_size_bytes(
verification);
char *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;
EvidencePreviewResult *result = result_new(request, path, size);
gsize read_limit = (gsize) MIN(size, EVIDENCE_PREVIEW_MAX_TEXT_BYTES);
guint8 *data = g_malloc(read_limit + 1); gsize read_size = 0;
GFile *file = g_file_new_for_path(path);
GFileInputStream *stream = g_file_read(file, cancellable, error);
if (stream == NULL || !g_input_stream_read_all(G_INPUT_STREAM(stream),
data, read_limit, &read_size, cancellable, error)) goto failed;
data[read_size] = '\0'; g_object_unref(stream); stream = NULL;
PreviewFormat format = detect_format(data, read_size, path,
request->mime_type);
if ((format == FORMAT_PNG || format == FORMAT_JPEG ||
format == FORMAT_HEIF || format == FORMAT_HEIC) &&
size > EVIDENCE_PREVIEW_MAX_FILE_BYTES) {
result->kind = EVIDENCE_PREVIEW_KIND_IMAGE;
result->message = g_strdup(
"Fichier importable, mais aperçu désactivé en raison de sa taille.");
goto done;
}
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;
gboolean ok = TRUE;
switch (format) {
case FORMAT_PNG:
result->kind = EVIDENCE_PREVIEW_KIND_IMAGE;
result->effective_mime_type = g_strdup("image/png");
result->effective_format = g_strdup("PNG");
ok = load_png(path, result, error); break;
case FORMAT_JPEG:
result->kind = EVIDENCE_PREVIEW_KIND_IMAGE;
result->effective_mime_type = g_strdup("image/jpeg");
result->effective_format = g_strdup("JPEG");
ok = load_jpeg(path, result, cancellable, error); break;
case FORMAT_HEIC: case FORMAT_HEIF:
result->kind = EVIDENCE_PREVIEW_KIND_IMAGE;
result->effective_mime_type = g_strdup(format == FORMAT_HEIC
? "image/heic" : "image/heif");
result->effective_format = g_strdup(format == FORMAT_HEIC
? "HEIC" : "HEIF");
ok = load_heif(path, result, error); break;
case FORMAT_MP4: case FORMAT_MOV:
result->kind = EVIDENCE_PREVIEW_KIND_VIDEO;
result->effective_mime_type = g_strdup(format == FORMAT_MOV
? "video/quicktime" : "video/mp4");
result->effective_format = g_strdup(format == FORMAT_MOV
? "MOV" : "MP4");
result->preview_available = TRUE;
result->message = g_strdup("Vidéo reconnue. Lecture manuelle uniquement.");
break;
case FORMAT_TEXT:
result->kind = EVIDENCE_PREVIEW_KIND_TEXT;
result->effective_mime_type = g_strdup("text/plain");
result->effective_format = g_strdup("Texte brut");
result->text = g_utf8_make_valid((const char *) data, read_size);
result->truncated = size > read_size; result->preview_available = TRUE;
break;
case FORMAT_EML:
result->kind = EVIDENCE_PREVIEW_KIND_EMAIL;
result->effective_mime_type = g_strdup("message/rfc822");
result->effective_format = g_strdup("EML");
ok = load_email(path, data, read_size, result, error); break;
case FORMAT_PDF:
result->kind = EVIDENCE_PREVIEW_KIND_PDF;
result->effective_mime_type = g_strdup("application/pdf");
result->effective_format = g_strdup("PDF");
ok = load_pdf(path, result, error); break;
default:
result->message = g_strdup("Format non pris en charge ou contenu invalide.");
result->effective_format = g_strdup("Inconnu"); break;
}
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;
if (!ok) goto failed;
done:
g_free(data); g_object_unref(file); g_free(path);
evidence_integrity_verification_result_free(verification); return result;
failed:
if (stream != NULL) g_object_unref(stream);
evidence_preview_result_free(result); g_free(data); g_object_unref(file);
g_free(path); evidence_integrity_verification_result_free(verification);
return NULL;
}

194
src/core/evidence_staging.c Normal file
View file

@ -0,0 +1,194 @@
#include "core/evidence_staging.h"
#include "core/file_hash.h"
#include <errno.h>
#include <glib/gstdio.h>
#include <string.h>
#include <sys/stat.h>
struct EvidenceStaging {
gint reference_count;
char *directory;
GPtrArray *paths;
};
static GQuark staging_error(void)
{
return g_quark_from_static_string("evidence-staging-error");
}
static const char *suggest_type(const char *mime)
{
if (g_strcmp0(mime, "image/png") == 0) return "screenshot";
if (g_strcmp0(mime, "image/jpeg") == 0) return "photo";
if (g_strcmp0(mime, "application/pdf") == 0) return "document";
if (mime != NULL && g_str_has_prefix(mime, "video/")) return "video";
if (mime != NULL && g_str_has_prefix(mime, "audio/")) return "audio";
if (mime != NULL && g_str_has_prefix(mime, "text/")) return "text";
if (g_strcmp0(mime, "message/rfc822") == 0) return "email";
return "other";
}
EvidenceStaging *evidence_staging_new(GError **error)
{
EvidenceStaging *staging = g_new0(EvidenceStaging, 1);
staging->directory = g_dir_make_tmp("labfy-person-staging-XXXXXX", error);
if (staging->directory == NULL) {
g_free(staging);
return NULL;
}
staging->paths = g_ptr_array_new_with_free_func(g_free);
staging->reference_count = 1;
return staging;
}
EvidenceStaging *evidence_staging_ref(EvidenceStaging *staging)
{
if (staging != NULL) g_atomic_int_inc(&staging->reference_count);
return staging;
}
const char *evidence_staging_get_directory(const EvidenceStaging *staging)
{
return staging != NULL ? staging->directory : NULL;
}
static char *detect_mime(const char *path, const char *name)
{
gboolean uncertain = FALSE;
char header[512];
gsize length = 0;
char *content = NULL;
char *mime;
if (!g_file_get_contents(path, &content, &length, NULL)) length = 0;
length = MIN(length, sizeof header);
if (length > 0) memcpy(header, content, length);
g_free(content);
mime = g_content_type_guess(name,
length > 0 ? (const guchar *) header : NULL, length, &uncertain);
(void) uncertain;
if (mime == NULL) return g_strdup("application/octet-stream");
char *result = g_content_type_get_mime_type(mime);
g_free(mime);
return result != NULL ? result : g_strdup("application/octet-stream");
}
EvidenceStagingResult *evidence_staging_prepare(EvidenceStaging *staging,
const char *source_path, GCancellable *cancellable, GError **error)
{
struct stat metadata;
EvidenceStagingResult *result = NULL;
GFile *source = NULL, *destination = NULL;
char *source_hash = NULL, *copy_hash = NULL, *internal = NULL;
guint64 hashed_size = 0;
GDateTime *now = NULL;
if (staging == NULL || source_path == NULL ||
g_file_test(source_path, G_FILE_TEST_IS_SYMLINK) ||
g_stat(source_path, &metadata) != 0 ||
!S_ISREG(metadata.st_mode)) {
g_set_error_literal(error, staging_error(), 1,
"Le fichier source est absent, spécial ou non autorisé.");
return NULL;
}
if ((guint64) metadata.st_size > EVIDENCE_STAGING_MAX_BYTES) {
g_set_error_literal(error, staging_error(), 2,
"Le fichier dépasse la taille maximale de staging.");
return NULL;
}
if (!file_hash_compute_sha256(source_path, cancellable,
&source_hash, &hashed_size, error)) goto cleanup;
{
char *identifier = g_uuid_string_random();
internal = g_strdup_printf("%s.stage", identifier);
g_free(identifier);
}
result = g_new0(EvidenceStagingResult, 1);
result->source_path = g_canonicalize_filename(source_path, NULL);
result->staging_path =
g_build_filename(staging->directory, internal, NULL);
result->original_name = g_path_get_basename(source_path);
source = g_file_new_for_path(source_path);
destination = g_file_new_for_path(result->staging_path);
if (!g_file_copy(source, destination, G_FILE_COPY_NONE,
cancellable, NULL, NULL, error)) goto cleanup;
if (!file_hash_compute_sha256(result->staging_path, cancellable,
&copy_hash, &hashed_size, error) ||
g_strcmp0(source_hash, copy_hash) != 0) {
if (error != NULL && *error == NULL)
g_set_error_literal(error, staging_error(), 3,
"La copie de staging ne correspond pas à la source.");
goto cleanup;
}
result->mime_type = detect_mime(result->staging_path,
result->original_name);
result->suggested_type = g_strdup(suggest_type(result->mime_type));
result->sha256 = g_steal_pointer(&copy_hash);
result->size_bytes = (guint64) metadata.st_size;
now = g_date_time_new_now_utc();
result->prepared_at = g_date_time_format(now, "%Y-%m-%dT%H:%M:%SZ");
g_ptr_array_add(staging->paths, g_strdup(result->staging_path));
cleanup:
if (result != NULL && result->sha256 == NULL) {
g_unlink(result->staging_path);
evidence_staging_result_free(result);
result = NULL;
}
g_clear_object(&source);
g_clear_object(&destination);
g_clear_pointer(&now, g_date_time_unref);
g_free(source_hash);
g_free(copy_hash);
g_free(internal);
return result;
}
void evidence_staging_result_free(EvidenceStagingResult *result)
{
if (result == NULL) return;
g_free(result->source_path);
g_free(result->staging_path);
g_free(result->original_name);
g_free(result->mime_type);
g_free(result->suggested_type);
g_free(result->sha256);
g_free(result->prepared_at);
g_free(result);
}
gboolean evidence_staging_remove(EvidenceStaging *staging,
const char *staging_path, GError **error)
{
if (staging == NULL || staging_path == NULL ||
!g_str_has_prefix(staging_path, staging->directory)) return FALSE;
if (g_unlink(staging_path) != 0 && errno != ENOENT) {
g_set_error(error, staging_error(), 4,
"Impossible de supprimer la copie de staging : %s.",
g_strerror(errno));
return FALSE;
}
for (guint i = 0; i < staging->paths->len; i++)
if (g_strcmp0(g_ptr_array_index(staging->paths, i),
staging_path) == 0) {
g_ptr_array_remove_index(staging->paths, i);
break;
}
return TRUE;
}
gboolean evidence_staging_cleanup(EvidenceStaging *staging, GError **error)
{
gboolean success = TRUE;
if (staging == NULL) return TRUE;
while (staging->paths->len > 0) {
char *path = g_strdup(g_ptr_array_index(staging->paths, 0));
if (!evidence_staging_remove(staging, path, error)) success = FALSE;
g_free(path);
if (!success) break;
}
if (success && staging->directory != NULL &&
g_rmdir(staging->directory) != 0 && errno != ENOENT) {
g_set_error(error, staging_error(), 5,
"Impossible de supprimer le répertoire de staging : %s.",
g_strerror(errno));
success = FALSE;
}
return success;
}
void evidence_staging_free(EvidenceStaging *staging)
{
if (staging == NULL) return;
if (!g_atomic_int_dec_and_test(&staging->reference_count)) return;
evidence_staging_cleanup(staging, NULL);
g_ptr_array_unref(staging->paths);
g_free(staging->directory);
g_free(staging);
}

View file

@ -0,0 +1,66 @@
#include "core/evidence_staging_task.h"
typedef struct {
EvidenceStaging *staging;
GPtrArray *paths;
} StagingTaskRequest;
static void request_free(gpointer data)
{
StagingTaskRequest *request = data;
if (request == NULL) return;
evidence_staging_free(request->staging);
g_ptr_array_unref(request->paths);
g_free(request);
}
static gboolean worker(BackgroundTask *task, GCancellable *cancellable,
gpointer data, gpointer *result, GError **error)
{
StagingTaskRequest *request = data;
GPtrArray *prepared = g_ptr_array_new_with_free_func(
(GDestroyNotify) evidence_staging_result_free);
for (guint i = 0; i < request->paths->len; i++) {
EvidenceStagingResult *item;
if (g_cancellable_set_error_if_cancelled(cancellable, error)) {
g_ptr_array_unref(prepared);
return FALSE;
}
item = evidence_staging_prepare(request->staging,
g_ptr_array_index(request->paths, i), cancellable, error);
if (item == NULL) {
g_ptr_array_unref(prepared);
return FALSE;
}
g_ptr_array_add(prepared, item);
background_task_report_progress(task,
(double) (i + 1) / request->paths->len,
"Préparation des copies de staging");
}
*result = prepared;
return TRUE;
}
BackgroundTask *evidence_staging_task_start(TaskManager *manager,
EvidenceStaging *staging, const GPtrArray *source_paths,
BackgroundTaskCompletionCallback callback, gpointer callback_data,
GDestroyNotify callback_data_destroy, GError **error)
{
BackgroundTask *task;
StagingTaskRequest *request;
if (manager == NULL || staging == NULL || source_paths == NULL ||
source_paths->len == 0) return NULL;
task = background_task_new("Préparation des preuves");
request = g_new0(StagingTaskRequest, 1);
request->staging = evidence_staging_ref(staging);
request->paths = g_ptr_array_new_with_free_func(g_free);
for (guint i = 0; i < source_paths->len; i++)
g_ptr_array_add(request->paths,
g_strdup(g_ptr_array_index((GPtrArray *) source_paths, i)));
if (task == NULL || !task_manager_add(manager, task, error) ||
!background_task_start(task, worker, request, request_free,
(GDestroyNotify) g_ptr_array_unref, callback, callback_data,
callback_data_destroy, error)) {
request_free(request);
background_task_unref(task);
return NULL;
}
return task;
}

View file

@ -0,0 +1,50 @@
#include "core/evidence_video_preview_controller.h"
struct EvidenceVideoPreviewController {
gpointer owner;
gpointer media;
EvidenceVideoPreviewActions actions;
};
EvidenceVideoPreviewController *evidence_video_preview_controller_new(
gpointer owner, const EvidenceVideoPreviewActions *actions)
{
if (actions == NULL || actions->pause == NULL ||
actions->detach == NULL || actions->release == NULL) return NULL;
EvidenceVideoPreviewController *controller = g_new0(
EvidenceVideoPreviewController, 1);
controller->owner = owner; controller->actions = *actions;
return controller;
}
void evidence_video_preview_controller_stop(
EvidenceVideoPreviewController *controller)
{
if (controller == NULL || controller->media == NULL) return;
controller->actions.pause(controller->media);
if (controller->actions.seek_start != NULL)
controller->actions.seek_start(controller->media);
if (controller->owner != NULL)
controller->actions.detach(controller->owner);
controller->actions.release(controller->media);
controller->media = NULL;
}
void evidence_video_preview_controller_replace(
EvidenceVideoPreviewController *controller, gpointer media)
{
if (controller == NULL) return;
evidence_video_preview_controller_stop(controller);
controller->media = media;
}
void evidence_video_preview_controller_free(
EvidenceVideoPreviewController *controller)
{
if (controller == NULL) return;
evidence_video_preview_controller_stop(controller); g_free(controller);
}
void evidence_video_preview_controller_abandon_owner(
EvidenceVideoPreviewController *controller)
{
if (controller == NULL) return;
controller->owner = NULL;
}
gboolean evidence_video_preview_controller_has_media(
const EvidenceVideoPreviewController *controller)
{ return controller != NULL && controller->media != NULL; }

View file

@ -76,3 +76,69 @@ char *person_confirmation_summary_build(const char *designation,
g_free(short_sha);
return g_string_free(summary, FALSE);
}
char *person_confirmation_summary_build_multiple(const char *designation,
const char *declared_name, const char *pseudonym,
const char *identification_status, gint confidence, const char *notes,
const GPtrArray *role_labels,
const PersonEvidenceSelection *selection)
{
GString *summary = g_string_new(NULL);
guint existing_count = 0, staged_count = 0;
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\n",
optional(g_ptr_array_index((GPtrArray *) role_labels, i),
"Rôle non renseigné"));
g_string_append(summary, "\nPREUVES\n");
if (person_evidence_selection_get_count(selection) == 0)
g_string_append(summary, "Aucune preuve retenue.\n");
for (guint i = 0; i < person_evidence_selection_get_count(selection);
i++) {
const PersonEvidenceSelectionItem *item =
person_evidence_selection_get(selection, i);
char *size = g_format_size(
person_evidence_selection_item_get_size_bytes(item));
char *short_sha = g_strndup(
person_evidence_selection_item_get_sha256(item), 12);
gboolean staged = person_evidence_selection_item_get_origin(item) ==
PERSON_EVIDENCE_ORIGIN_STAGED;
if (staged) staged_count++; else existing_count++;
g_string_append_printf(summary,
"• %s\n Origine : %s\n Type métier : %s\n MIME : %s\n"
" Taille : %s\n SHA-256 : %s…\n Intégrité : Valide\n"
" Description : %s\n Statut : %s\n",
optional(person_evidence_selection_item_get_original_name(item),
"Non renseigné"),
staged ? "Nouvelle" : "Existante",
optional(person_evidence_selection_item_get_type_identifier(item),
"Non renseigné"),
optional(person_evidence_selection_item_get_mime_type(item),
"Non renseigné"), size, optional(short_sha, "Non renseigné"),
optional(person_evidence_selection_item_get_description(item),
"Aucune"),
staged ? "Prête à importer" : "Déjà importée");
g_free(size);
g_free(short_sha);
}
g_string_append_printf(summary,
"\nAVERTISSEMENTS\n"
"%u nouvelle(s) preuve(s) à importer.\n"
"%u preuve(s) existante(s) à rattacher.\n"
"Aucune écriture na encore été effectuée.\n"
"Lannulation ne réalise aucun import définitif.\n"
"Aucun OCR ne sera lancé dans cette tranche.",
staged_count, existing_count);
return g_string_free(summary, FALSE);
}

View file

@ -0,0 +1,276 @@
#include "core/person_creation_coordinator.h"
#include "core/file_hash.h"
#include "dao/entity_dao.h"
#include "dao/evidence_dao.h"
#include "dao/evidence_entity_dao.h"
#include "dao/person_role_assignment_dao.h"
#include "database/transaction.h"
#include <errno.h>
#include <glib/gstdio.h>
#include <string.h>
static GQuark coordinator_error(void)
{
return g_quark_from_static_string("person-creation-coordinator-error");
}
static gboolean cancelled(GCancellable *cancellable, GError **error)
{
return cancellable != NULL &&
g_cancellable_set_error_if_cancelled(cancellable, error);
}
static char *timestamp_now(void)
{
GDateTime *now = g_date_time_new_now_utc();
char *timestamp = now != NULL
? g_date_time_format(now, "%Y-%m-%dT%H:%M:%SZ") : NULL;
g_clear_pointer(&now, g_date_time_unref);
return timestamp;
}
static char *safe_extension(const char *name)
{
const char *dot = name != NULL ? strrchr(name, '.') : NULL;
if (dot == NULL || strlen(dot) > 12) return g_strdup("");
for (const char *cursor = dot + 1; *cursor != '\0'; cursor++)
if (!g_ascii_isalnum(*cursor)) return g_strdup("");
return g_ascii_strdown(dot, -1);
}
static gboolean copy_staged(const char *root,
const PersonEvidenceSelectionItem *item, char **out_relative,
char **out_internal, char **out_absolute, GCancellable *cancellable,
GError **error)
{
const char *type =
person_evidence_selection_item_get_type_identifier(item);
char *extension = safe_extension(
person_evidence_selection_item_get_original_name(item));
char *uuid = g_uuid_string_random();
char *directory = g_build_filename(
root, "01_Preuves_Originales", type, NULL);
GFile *source = NULL, *destination = NULL;
char *computed = NULL;
guint64 computed_size = 0;
gboolean success = FALSE;
*out_internal = g_strconcat(uuid, extension, NULL);
*out_relative = g_build_filename("01_Preuves_Originales", type,
*out_internal, NULL);
*out_absolute = g_build_filename(root, *out_relative, NULL);
if (g_mkdir_with_parents(directory, 0700) != 0) {
g_set_error(error, coordinator_error(), 1,
"Impossible de préparer le dossier définitif : %s.",
g_strerror(errno));
goto cleanup;
}
source = g_file_new_for_path(
person_evidence_selection_item_get_staging_path(item));
destination = g_file_new_for_path(*out_absolute);
if (!g_file_copy(source, destination, G_FILE_COPY_NONE, cancellable,
NULL, NULL, error)) goto cleanup;
if (!file_hash_compute_sha256(*out_absolute, cancellable,
&computed, &computed_size, error) ||
g_strcmp0(computed,
person_evidence_selection_item_get_sha256(item)) != 0) {
if (error != NULL && *error == NULL)
g_set_error_literal(error, coordinator_error(), 2,
"La copie définitive ne correspond pas au staging.");
goto cleanup;
}
success = TRUE;
cleanup:
if (!success && *out_absolute != NULL) g_unlink(*out_absolute);
g_clear_object(&source);
g_clear_object(&destination);
g_free(computed);
g_free(extension);
g_free(uuid);
g_free(directory);
return success;
}
static EntityRecord *build_person(const PersonEntityInput *input,
const char *identifier, const char *timestamp, GError **error)
{
const char *value = input->declared_name != NULL &&
input->declared_name[0] != '\0' ? input->declared_name :
input->pseudonym != NULL && input->pseudonym[0] != '\0'
? input->pseudonym : input->designation;
char *description = g_strdup_printf(
"Statut d'identification : %s.%s%s%s%s",
input->identification_status,
input->pseudonym != NULL ? "\nPseudonyme déclaré : " : "",
input->pseudonym != NULL ? input->pseudonym : "",
input->notes != NULL ? "\nNotes factuelles : " : "",
input->notes != NULL ? input->notes : "");
EntityRecord *record = entity_record_new(identifier, "person", value,
input->designation, description, input->confidence,
timestamp, timestamp, ENTITY_STATUS_ACTIVE, error);
g_free(description);
return record;
}
static gboolean validate_selection_files(const char *root,
const PersonEvidenceSelection *selection, GCancellable *cancellable,
GError **error)
{
for (guint i = 0; i < person_evidence_selection_get_count(selection);
i++) {
const PersonEvidenceSelectionItem *item =
person_evidence_selection_get(selection, i);
const char *expected =
person_evidence_selection_item_get_sha256(item);
char *path = NULL, *computed = NULL;
guint64 size = 0;
if (person_evidence_selection_item_get_origin(item) ==
PERSON_EVIDENCE_ORIGIN_EXISTING) {
const EvidenceRecord *record =
person_evidence_selection_item_get_record(item);
path = g_build_filename(root,
evidence_record_get_relative_path(record), NULL);
} else
path = g_strdup(
person_evidence_selection_item_get_staging_path(item));
if (!file_hash_compute_sha256(path, cancellable,
&computed, &size, error) ||
g_strcmp0(computed, expected) != 0 ||
size != person_evidence_selection_item_get_size_bytes(item)) {
if (error != NULL && *error == NULL)
g_set_error_literal(error, coordinator_error(), 3,
"Une preuve a changé depuis sa sélection.");
g_free(path); g_free(computed);
return FALSE;
}
g_free(path); g_free(computed);
}
return TRUE;
}
PersonCreationCoordinatorResult *person_creation_coordinator_execute(
Database *database, const char *root, const PersonEntityInput *person,
const PersonEvidenceSelection *selection, GCancellable *cancellable,
GError **error)
{
PersonCreationCoordinatorResult *result = NULL;
EntityDao *entity_dao = NULL;
EvidenceDao *evidence_dao = NULL;
EvidenceEntityDao *link_dao = NULL;
PersonRoleAssignmentDao *role_dao = NULL;
EntityRecord *person_record = NULL;
GPtrArray *created_paths = g_ptr_array_new_with_free_func(g_free);
char *timestamp = NULL;
gboolean transaction_active = FALSE, success = FALSE;
if (database == NULL || root == NULL || person == NULL ||
person->designation == NULL || person->designation[0] == '\0' ||
!person_evidence_selection_is_confirmable(selection)) {
g_set_error_literal(error, coordinator_error(), 1,
"La création de personne ou la sélection est invalide.");
goto cleanup;
}
if (cancelled(cancellable, error)) goto cleanup;
if (!validate_selection_files(root, selection, cancellable, error))
goto cleanup;
timestamp = timestamp_now();
result = g_new0(PersonCreationCoordinatorResult, 1);
result->person_identifier = g_uuid_string_random();
result->evidence_identifiers =
g_ptr_array_new_with_free_func(g_free);
entity_dao = entity_dao_new(database, error);
evidence_dao = evidence_dao_new(database, error);
link_dao = evidence_entity_dao_new(database, error);
role_dao = person_role_assignment_dao_new(database, error);
person_record = build_person(person, result->person_identifier,
timestamp, error);
if (timestamp == NULL || entity_dao == NULL || evidence_dao == NULL ||
link_dao == NULL || role_dao == NULL || person_record == NULL ||
!database_transaction_begin(database)) goto cleanup;
transaction_active = TRUE;
if (!entity_dao_insert(entity_dao, person_record, error) ||
!person_role_assignment_dao_insert_all(role_dao,
result->person_identifier, person->role_assignments, error))
goto cleanup;
for (guint i = 0; i < person_evidence_selection_get_count(selection);
i++) {
const PersonEvidenceSelectionItem *item =
person_evidence_selection_get(selection, i);
const char *evidence_identifier =
person_evidence_selection_item_get_evidence_identifier(item);
char *identifier = NULL, *relative = NULL, *internal = NULL;
char *absolute = NULL, *duplicate_identifier = NULL;
EvidenceRecord *record = NULL;
if (cancelled(cancellable, error)) goto cleanup;
if (person_evidence_selection_item_get_origin(item) ==
PERSON_EVIDENCE_ORIGIN_STAGED) {
duplicate_identifier = evidence_dao_find_identifier_by_sha256(
evidence_dao,
person_evidence_selection_item_get_sha256(item), error);
if (error != NULL && *error != NULL) {
g_free(duplicate_identifier);
goto cleanup;
}
if (duplicate_identifier != NULL)
evidence_identifier = duplicate_identifier;
else {
identifier = g_uuid_string_random();
if (!copy_staged(root, item, &relative, &internal, &absolute,
cancellable, error)) {
g_free(identifier); g_free(relative); g_free(internal);
g_free(absolute); goto cleanup;
}
g_ptr_array_add(created_paths, g_strdup(absolute));
record = evidence_record_new(identifier,
person_evidence_selection_item_get_original_name(item),
internal, relative,
person_evidence_selection_item_get_type_identifier(item),
person_evidence_selection_item_get_size_bytes(item),
person_evidence_selection_item_get_sha256(item), timestamp,
NULL, "Import depuis lassistant de création dune personne",
person_evidence_selection_item_get_description(item),
EVIDENCE_INTEGRITY_STATUS_VALID, error);
if (record == NULL ||
!evidence_dao_insert(evidence_dao, record, error)) {
evidence_record_free(record); g_free(identifier);
g_free(relative); g_free(internal); g_free(absolute);
goto cleanup;
}
evidence_identifier = identifier;
}
}
if (!evidence_entity_dao_link(link_dao, evidence_identifier,
result->person_identifier, error)) {
evidence_record_free(record); g_free(identifier);
g_free(duplicate_identifier);
g_free(relative); g_free(internal); g_free(absolute);
goto cleanup;
}
g_ptr_array_add(result->evidence_identifiers,
g_strdup(evidence_identifier));
evidence_record_free(record);
g_free(duplicate_identifier);
g_free(identifier); g_free(relative); g_free(internal);
g_free(absolute);
}
if (!database_transaction_commit(database)) goto cleanup;
transaction_active = FALSE;
success = TRUE;
cleanup:
if (!success && transaction_active)
database_transaction_rollback(database);
if (!success)
for (guint i = 0; i < created_paths->len; i++)
g_unlink(g_ptr_array_index(created_paths, i));
g_ptr_array_unref(created_paths);
entity_record_free(person_record);
entity_dao_free(entity_dao);
evidence_dao_free(evidence_dao);
evidence_entity_dao_free(link_dao);
person_role_assignment_dao_free(role_dao);
g_free(timestamp);
if (!success) {
person_creation_coordinator_result_free(result);
result = NULL;
}
return result;
}
void person_creation_coordinator_result_free(
PersonCreationCoordinatorResult *result)
{
if (result == NULL) return;
g_free(result->person_identifier);
g_clear_pointer(&result->evidence_identifiers, g_ptr_array_unref);
g_free(result);
}

View file

@ -0,0 +1,98 @@
#include "core/person_creation_task.h"
struct PersonCreationTaskRequest {
char *database_path;
char *root;
PersonEntityInput person;
char *designation;
char *declared_name;
char *pseudonym;
char *status;
char *notes;
GPtrArray *roles;
PersonEvidenceSelection *selection;
};
PersonCreationTaskRequest *person_creation_task_request_new(
const char *database_path, const char *root,
const PersonEntityInput *person,
const PersonEvidenceSelection *selection)
{
PersonCreationTaskRequest *request;
if (database_path == NULL || root == NULL || person == NULL) return NULL;
request = g_new0(PersonCreationTaskRequest, 1);
request->database_path = g_strdup(database_path);
request->root = g_strdup(root);
request->designation = g_strdup(person->designation);
request->declared_name = g_strdup(person->declared_name);
request->pseudonym = g_strdup(person->pseudonym);
request->status = g_strdup(person->identification_status);
request->notes = g_strdup(person->notes);
request->roles = g_ptr_array_new_with_free_func(
(GDestroyNotify) person_role_assignment_input_free);
for (guint i = 0; person->role_assignments != NULL &&
i < person->role_assignments->len; i++)
g_ptr_array_add(request->roles,
person_role_assignment_input_copy(g_ptr_array_index(
(GPtrArray *) person->role_assignments, i)));
request->person = *person;
request->person.designation = request->designation;
request->person.declared_name = request->declared_name;
request->person.pseudonym = request->pseudonym;
request->person.identification_status = request->status;
request->person.notes = request->notes;
request->person.evidence_identifier = NULL;
request->person.role_assignments = request->roles;
request->selection = person_evidence_selection_copy(selection);
return request;
}
void person_creation_task_request_free(PersonCreationTaskRequest *request)
{
if (request == NULL) return;
g_free(request->database_path); g_free(request->root);
g_free(request->designation); g_free(request->declared_name);
g_free(request->pseudonym); g_free(request->status);
g_free(request->notes); g_ptr_array_unref(request->roles);
person_evidence_selection_free(request->selection);
g_free(request);
}
static gboolean worker(BackgroundTask *task, GCancellable *cancellable,
gpointer data, gpointer *result, GError **error)
{
PersonCreationTaskRequest *request = data;
Database *database;
(void) task;
database = database_open(request->database_path);
if (database == NULL) {
g_set_error_literal(error,
g_quark_from_static_string("person-creation-task-error"), 1,
"Impossible douvrir la base de lenquête.");
return FALSE;
}
*result = person_creation_coordinator_execute(database, request->root,
&request->person, request->selection, cancellable, error);
database_close(database);
return *result != NULL;
}
BackgroundTask *person_creation_task_start(TaskManager *manager,
const PersonCreationTaskRequest *request,
BackgroundTaskCompletionCallback callback, gpointer callback_data,
GDestroyNotify callback_data_destroy, GError **error)
{
BackgroundTask *task;
PersonCreationTaskRequest *copy;
if (manager == NULL || request == NULL) return NULL;
task = background_task_new("Création de la personne et import des preuves");
copy = person_creation_task_request_new(request->database_path,
request->root, &request->person, request->selection);
if (task == NULL || copy == NULL ||
!task_manager_add(manager, task, error) ||
!background_task_start(task, worker, copy,
(GDestroyNotify) person_creation_task_request_free,
(GDestroyNotify) person_creation_coordinator_result_free,
callback, callback_data, callback_data_destroy, error)) {
person_creation_task_request_free(copy);
background_task_unref(task);
return NULL;
}
return task;
}

View file

@ -1562,6 +1562,38 @@ cleanup:
return success;
}
char *evidence_dao_find_identifier_by_sha256(EvidenceDao *evidence_dao,
const char *sha256, GError **error)
{
static const char sql[] =
"SELECT id FROM preuves WHERE sha256=? ORDER BY imported_at,id LIMIT 1;";
DatabaseStatement *statement;
char *identifier = NULL;
if (evidence_dao == NULL || sha256 == NULL || strlen(sha256) != 64) {
evidence_dao_set_error_literal(error,
EVIDENCE_DAO_ERROR_INVALID_ARGUMENT,
"Lempreinte recherchée est invalide.");
return NULL;
}
statement = database_statement_prepare(evidence_dao->database, sql);
if (statement == NULL ||
!database_statement_bind_text(statement, 1, sha256)) {
evidence_dao_set_database_error(evidence_dao, error,
statement == NULL ? EVIDENCE_DAO_ERROR_PREPARE :
EVIDENCE_DAO_ERROR_BIND,
"Impossible de rechercher le doublon de preuve");
database_statement_finalize(statement);
return NULL;
}
if (database_statement_step(statement) == DATABASE_STATEMENT_STEP_ROW &&
!database_statement_column_text(statement, 0, &identifier))
evidence_dao_set_database_error(evidence_dao, error,
EVIDENCE_DAO_ERROR_READ,
"Impossible de lire le doublon de preuve");
database_statement_finalize(statement);
return identifier;
}
GPtrArray *evidence_dao_list_all(
EvidenceDao *evidence_dao,
GError **error

View file

@ -136,3 +136,25 @@ GPtrArray *evidence_selection_model_list_type_codes(
g_ptr_array_sort(types, compare_strings);
return types;
}
const EvidenceRecord *evidence_selection_model_find_by_sha256(
const EvidenceSelectionModel *model, const char *sha256)
{
for (guint i = 0; model != NULL && sha256 != NULL &&
i < model->records->len; i++) {
EvidenceRecord *record = g_ptr_array_index(model->records, i);
if (g_strcmp0(evidence_record_get_sha256(record), sha256) == 0)
return record;
}
return NULL;
}
const EvidenceRecord *evidence_selection_model_find_by_identifier(
const EvidenceSelectionModel *model, const char *identifier)
{
for (guint i = 0; model != NULL && identifier != NULL &&
i < model->records->len; i++) {
EvidenceRecord *record = g_ptr_array_index(model->records, i);
if (g_strcmp0(evidence_record_get_identifier(record), identifier) == 0)
return record;
}
return NULL;
}

View file

@ -0,0 +1,295 @@
#include "models/person_evidence_selection.h"
struct PersonEvidenceSelectionItem {
char *identifier;
PersonEvidenceOrigin origin;
PersonEvidenceState state;
EvidenceRecord *record;
char *evidence_identifier;
char *source_path;
char *staging_path;
char *original_name;
char *mime_type;
char *type_identifier;
char *sha256;
char *description;
char *prepared_at;
guint64 size_bytes;
};
struct PersonEvidenceSelection {
GPtrArray *items;
char *active_identifier;
};
static GQuark selection_error(void)
{
return g_quark_from_static_string("person-evidence-selection-error");
}
static void item_free(gpointer data)
{
PersonEvidenceSelectionItem *item = data;
if (item == NULL) return;
g_free(item->identifier);
evidence_record_free(item->record);
g_free(item->evidence_identifier);
g_free(item->source_path);
g_free(item->staging_path);
g_free(item->original_name);
g_free(item->mime_type);
g_free(item->type_identifier);
g_free(item->sha256);
g_free(item->description);
g_free(item->prepared_at);
g_free(item);
}
static PersonEvidenceSelectionItem *find_item(
const PersonEvidenceSelection *selection, const char *identifier)
{
for (guint i = 0; selection != NULL && identifier != NULL &&
i < selection->items->len; i++) {
PersonEvidenceSelectionItem *item =
g_ptr_array_index(selection->items, i);
if (g_strcmp0(item->identifier, identifier) == 0) return item;
}
return NULL;
}
static gboolean duplicate(const PersonEvidenceSelection *selection,
const char *evidence_identifier, const char *source_path,
const char *sha256)
{
for (guint i = 0; selection != NULL && i < selection->items->len; i++) {
PersonEvidenceSelectionItem *item =
g_ptr_array_index(selection->items, i);
if ((evidence_identifier != NULL &&
g_strcmp0(item->evidence_identifier, evidence_identifier) == 0) ||
(source_path != NULL &&
g_strcmp0(item->source_path, source_path) == 0) ||
(sha256 != NULL && g_strcmp0(item->sha256, sha256) == 0))
return TRUE;
}
return FALSE;
}
PersonEvidenceSelection *person_evidence_selection_new(void)
{
PersonEvidenceSelection *selection =
g_new0(PersonEvidenceSelection, 1);
selection->items = g_ptr_array_new_with_free_func(item_free);
return selection;
}
PersonEvidenceSelection *person_evidence_selection_copy(
const PersonEvidenceSelection *source)
{
PersonEvidenceSelection *copy = person_evidence_selection_new();
for (guint i = 0; source != NULL && i < source->items->len; i++) {
PersonEvidenceSelectionItem *item =
g_ptr_array_index(source->items, i);
if (item->origin == PERSON_EVIDENCE_ORIGIN_EXISTING) {
if (person_evidence_selection_add_existing(
copy, item->record, NULL))
person_evidence_selection_set_type(copy, item->identifier,
item->type_identifier);
} else
person_evidence_selection_add_staged(copy, item->source_path,
item->staging_path, item->original_name, item->mime_type,
item->type_identifier, item->size_bytes, item->sha256,
item->description, item->prepared_at, NULL);
}
if (source != NULL && source->active_identifier != NULL) {
PersonEvidenceSelectionItem *active =
find_item(source, source->active_identifier);
for (guint i = 0; active != NULL && i < copy->items->len; i++) {
PersonEvidenceSelectionItem *candidate =
g_ptr_array_index(copy->items, i);
if (g_strcmp0(candidate->evidence_identifier,
active->evidence_identifier) == 0 ||
(candidate->source_path != NULL &&
g_strcmp0(candidate->source_path, active->source_path) == 0))
person_evidence_selection_set_active(
copy, candidate->identifier);
}
}
return copy;
}
void person_evidence_selection_free(PersonEvidenceSelection *selection)
{
if (selection == NULL) return;
g_ptr_array_unref(selection->items);
g_free(selection->active_identifier);
g_free(selection);
}
guint person_evidence_selection_get_count(
const PersonEvidenceSelection *selection)
{
return selection != NULL ? selection->items->len : 0;
}
const PersonEvidenceSelectionItem *person_evidence_selection_get(
const PersonEvidenceSelection *selection, guint index)
{
return selection != NULL && index < selection->items->len
? g_ptr_array_index(selection->items, index) : NULL;
}
const PersonEvidenceSelectionItem *person_evidence_selection_get_active(
const PersonEvidenceSelection *selection)
{
return find_item(selection,
selection != NULL ? selection->active_identifier : NULL);
}
gboolean person_evidence_selection_set_active(
PersonEvidenceSelection *selection, const char *item_identifier)
{
if (selection == NULL ||
(item_identifier != NULL &&
find_item(selection, item_identifier) == NULL)) return FALSE;
g_free(selection->active_identifier);
selection->active_identifier = g_strdup(item_identifier);
return TRUE;
}
gboolean person_evidence_selection_add_existing(
PersonEvidenceSelection *selection, const EvidenceRecord *record,
GError **error)
{
PersonEvidenceSelectionItem *item;
const char *identifier;
if (selection == NULL || record == NULL ||
(identifier = evidence_record_get_identifier(record)) == NULL) {
g_set_error_literal(error, selection_error(), 1,
"La preuve existante est invalide.");
return FALSE;
}
if (duplicate(selection, identifier, NULL,
evidence_record_get_sha256(record))) {
g_set_error_literal(error, selection_error(), 2,
"Cette preuve est déjà dans la sélection.");
return FALSE;
}
item = g_new0(PersonEvidenceSelectionItem, 1);
item->identifier = g_strdup(identifier);
item->origin = PERSON_EVIDENCE_ORIGIN_EXISTING;
item->state = PERSON_EVIDENCE_STATE_READY;
item->record = evidence_record_copy(record);
item->evidence_identifier = g_strdup(identifier);
item->original_name = g_strdup(evidence_record_get_original_name(record));
item->mime_type = g_strdup(evidence_record_get_mime_type(record));
item->type_identifier =
g_strdup(evidence_record_get_type_identifier(record));
item->sha256 = g_strdup(evidence_record_get_sha256(record));
item->description = g_strdup(evidence_record_get_description(record));
item->size_bytes = evidence_record_get_size_bytes(record);
g_ptr_array_add(selection->items, item);
if (selection->active_identifier == NULL)
selection->active_identifier = g_strdup(item->identifier);
return TRUE;
}
gboolean person_evidence_selection_add_staged(
PersonEvidenceSelection *selection, const char *source_path,
const char *staging_path, const char *original_name,
const char *mime_type, const char *type_identifier, guint64 size_bytes,
const char *sha256, const char *description, const char *prepared_at,
GError **error)
{
PersonEvidenceSelectionItem *item;
if (selection == NULL || source_path == NULL || staging_path == NULL ||
original_name == NULL || mime_type == NULL || type_identifier == NULL ||
sha256 == NULL || prepared_at == NULL) {
g_set_error_literal(error, selection_error(), 1,
"Le fichier préparé est invalide.");
return FALSE;
}
if (duplicate(selection, NULL, source_path, sha256)) {
g_set_error_literal(error, selection_error(), 2,
"Ce contenu est déjà dans la sélection.");
return FALSE;
}
item = g_new0(PersonEvidenceSelectionItem, 1);
item->identifier = g_uuid_string_random();
item->origin = PERSON_EVIDENCE_ORIGIN_STAGED;
item->state = PERSON_EVIDENCE_STATE_READY;
item->source_path = g_strdup(source_path);
item->staging_path = g_strdup(staging_path);
item->original_name = g_strdup(original_name);
item->mime_type = g_strdup(mime_type);
item->type_identifier = g_strdup(type_identifier);
item->sha256 = g_strdup(sha256);
item->description = g_strdup(description);
item->prepared_at = g_strdup(prepared_at);
item->size_bytes = size_bytes;
g_ptr_array_add(selection->items, item);
if (selection->active_identifier == NULL)
selection->active_identifier = g_strdup(item->identifier);
return TRUE;
}
gboolean person_evidence_selection_remove(PersonEvidenceSelection *selection,
const char *item_identifier)
{
for (guint i = 0; selection != NULL && item_identifier != NULL &&
i < selection->items->len; i++) {
PersonEvidenceSelectionItem *item =
g_ptr_array_index(selection->items, i);
if (g_strcmp0(item->identifier, item_identifier) == 0) {
gboolean active = g_strcmp0(selection->active_identifier,
item_identifier) == 0;
g_ptr_array_remove_index(selection->items, i);
if (active) {
g_clear_pointer(&selection->active_identifier, g_free);
if (selection->items->len > 0)
selection->active_identifier = g_strdup(
((PersonEvidenceSelectionItem *) g_ptr_array_index(
selection->items, MIN(i,
selection->items->len - 1)))->identifier);
}
return TRUE;
}
}
return FALSE;
}
gboolean person_evidence_selection_set_type(PersonEvidenceSelection *selection,
const char *item_identifier, const char *type_identifier)
{
PersonEvidenceSelectionItem *item = find_item(selection, item_identifier);
if (item == NULL || type_identifier == NULL ||
type_identifier[0] == '\0') return FALSE;
g_free(item->type_identifier);
item->type_identifier = g_strdup(type_identifier);
return TRUE;
}
gboolean person_evidence_selection_is_confirmable(
const PersonEvidenceSelection *selection)
{
for (guint i = 0; selection != NULL && i < selection->items->len; i++)
if (((PersonEvidenceSelectionItem *) g_ptr_array_index(
selection->items, i))->state != PERSON_EVIDENCE_STATE_READY)
return FALSE;
return selection != NULL;
}
const char *person_evidence_selection_find_existing_by_sha256(
const PersonEvidenceSelection *selection, const char *sha256)
{
for (guint i = 0; selection != NULL && sha256 != NULL &&
i < selection->items->len; i++) {
PersonEvidenceSelectionItem *item =
g_ptr_array_index(selection->items, i);
if (item->origin == PERSON_EVIDENCE_ORIGIN_EXISTING &&
g_strcmp0(item->sha256, sha256) == 0)
return item->evidence_identifier;
}
return NULL;
}
#define ITEM_GETTER(name, type, field) \
type person_evidence_selection_item_get_##name( \
const PersonEvidenceSelectionItem *item) { \
return item != NULL ? item->field : (type) 0; \
}
ITEM_GETTER(identifier, const char *, identifier)
ITEM_GETTER(origin, PersonEvidenceOrigin, origin)
ITEM_GETTER(state, PersonEvidenceState, state)
ITEM_GETTER(record, const EvidenceRecord *, record)
ITEM_GETTER(evidence_identifier, const char *, evidence_identifier)
ITEM_GETTER(source_path, const char *, source_path)
ITEM_GETTER(staging_path, const char *, staging_path)
ITEM_GETTER(original_name, const char *, original_name)
ITEM_GETTER(mime_type, const char *, mime_type)
ITEM_GETTER(type_identifier, const char *, type_identifier)
ITEM_GETTER(sha256, const char *, sha256)
ITEM_GETTER(description, const char *, description)
ITEM_GETTER(prepared_at, const char *, prepared_at)
ITEM_GETTER(size_bytes, guint64, size_bytes)

View file

@ -3,10 +3,13 @@
* @brief Formulaire GTK de création d'une personne observée.
******************************************************************************/
#include "views/create_person_dialog.h"
#include "core/evidence_preview_task.h"
#include "core/evidence_staging.h"
#include "core/evidence_staging_task.h"
#include "core/person_confirmation_summary.h"
#include "core/person_dialog_lifecycle.h"
#include "models/evidence_selection_model.h"
#include "models/person_evidence_selection.h"
#include "widgets/evidence_preview_widget.h"
struct CreatePersonDialogResult
{
@ -18,6 +21,8 @@ struct CreatePersonDialogResult
char *notes;
char *evidence_identifier;
GPtrArray *role_assignments;
PersonEvidenceSelection *evidence_selection;
EvidenceStaging *staging;
};
typedef struct
{
@ -28,44 +33,57 @@ typedef struct
GtkDropDown *status;
GtkSpinButton *confidence;
GtkDropDown *evidence;
GtkDropDown *retained;
GtkDropDown *retained_type;
GtkDropDown *type_filter;
GtkEntry *search;
GtkTextView *notes;
GtkLabel *error;
GtkLabel *metadata;
GtkLabel *preview_status;
GtkPicture *preview_picture;
EvidencePreviewWidget *preview;
GtkLabel *progress;
GtkLabel *summary;
GtkStack *stack;
GtkButton *previous;
GtkButton *next;
GtkButton *create;
GtkButton *add_existing;
GtkButton *remove_retained;
GtkButton *import_files;
GtkStringList *evidence_labels;
GtkStringList *retained_labels;
GtkStringList *type_filter_labels;
GtkCheckButton *roles[9];
guint step;
GPtrArray *evidence_identifiers;
GPtrArray *visible_records;
GPtrArray *type_codes;
EvidenceSelectionModel *selection_model;
PersonEvidenceSelection *person_evidence_selection;
EvidenceStaging *staging;
char *investigation_root_path;
TaskManager *task_manager;
BackgroundTask *preview_task;
BackgroundTask *staging_task;
PersonDialogLifecycle *lifecycle;
CreatePersonDialogCallback callback;
CreatePersonDialogSessionCheck session_check;
gpointer user_data;
GDestroyNotify user_data_destroy;
gulong evidence_selected_handler;
gulong retained_selected_handler;
gboolean updating_evidence;
gboolean updating_retained;
} CreatePersonDialogState;
typedef struct {
GWeakRef window;
char *evidence_identifier;
guint64 generation;
} CreatePersonPreviewContext;
} CreatePersonStagingContext;
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);
const EvidenceRecord *record, const char *business_type);
static void create_person_dialog_on_retained_changed(
GtkDropDown *dropdown, GParamSpec *pspec, gpointer data);
/** @brief Copie et nettoie un texte facultatif. */
static char *create_person_dialog_copy(const char *text)
@ -89,45 +107,392 @@ static void create_person_dialog_state_free(gpointer data)
{
CreatePersonDialogState *state = data;
if (state == NULL) return;
if (state->preview_task != NULL) {
background_task_cancel(state->preview_task);
background_task_unref(state->preview_task);
g_clear_pointer(&state->preview, evidence_preview_widget_free);
if (state->staging_task != NULL) {
background_task_cancel(state->staging_task);
background_task_unref(state->staging_task);
}
person_dialog_lifecycle_cancel(state->lifecycle);
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->person_evidence_selection,
person_evidence_selection_free);
g_clear_pointer(&state->staging, evidence_staging_free);
g_clear_pointer(&state->lifecycle, person_dialog_lifecycle_free);
g_clear_object(&state->evidence_labels);
g_clear_object(&state->retained_labels);
g_clear_object(&state->type_filter_labels);
g_free(state->investigation_root_path);
if (state->user_data_destroy != NULL)
state->user_data_destroy(state->user_data);
g_free(state);
}
static void create_person_dialog_rebuild_retained(
CreatePersonDialogState *state)
{
GPtrArray *labels = g_ptr_array_new_with_free_func(g_free);
const PersonEvidenceSelectionItem *active =
person_evidence_selection_get_active(
state->person_evidence_selection);
const char *active_identifier = active != NULL
? person_evidence_selection_item_get_identifier(active) : NULL;
guint selected = GTK_INVALID_LIST_POSITION;
for (guint i = 0; i < person_evidence_selection_get_count(
state->person_evidence_selection); i++) {
const PersonEvidenceSelectionItem *item =
person_evidence_selection_get(state->person_evidence_selection, i);
char *label = g_strdup_printf("%s — %s — %s",
person_evidence_selection_item_get_original_name(item),
person_evidence_selection_item_get_origin(item) ==
PERSON_EVIDENCE_ORIGIN_STAGED ? "nouvelle" : "existante",
person_evidence_selection_item_get_type_identifier(item));
g_ptr_array_add(labels, label);
if (g_strcmp0(active_identifier,
person_evidence_selection_item_get_identifier(item)) == 0)
selected = i;
}
g_ptr_array_add(labels, NULL);
state->updating_retained = TRUE;
if (state->retained_selected_handler != 0)
g_signal_handler_block(state->retained,
state->retained_selected_handler);
gtk_string_list_splice(state->retained_labels, 0,
g_list_model_get_n_items(G_LIST_MODEL(state->retained_labels)),
(const char * const *) labels->pdata);
gtk_drop_down_set_selected(state->retained, selected);
if (state->retained_selected_handler != 0)
g_signal_handler_unblock(state->retained,
state->retained_selected_handler);
state->updating_retained = FALSE;
g_ptr_array_unref(labels);
if (selected != GTK_INVALID_LIST_POSITION)
create_person_dialog_on_retained_changed(
state->retained, NULL, state);
}
static void create_person_dialog_on_add_existing(
GtkButton *button, gpointer data)
{
CreatePersonDialogState *state = data;
const EvidenceRecord *record =
evidence_selection_model_get_selected(state->selection_model);
GError *error = NULL;
(void) button;
if (record == NULL && state->visible_records->len == 1)
record = g_ptr_array_index(state->visible_records, 0);
if (record == NULL) {
gtk_label_set_text(state->error,
"Sélectionnez dabord une preuve existante.");
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
return;
}
if (!person_evidence_selection_add_existing(
state->person_evidence_selection, record, &error)) {
gtk_label_set_text(state->error, error->message);
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
} else {
gtk_widget_set_visible(GTK_WIDGET(state->error), FALSE);
create_person_dialog_rebuild_retained(state);
}
g_clear_error(&error);
}
static void create_person_dialog_on_retained_changed(
GtkDropDown *dropdown, GParamSpec *pspec, gpointer data)
{
CreatePersonDialogState *state = data;
guint selected = gtk_drop_down_get_selected(dropdown);
const PersonEvidenceSelectionItem *item =
person_evidence_selection_get(state->person_evidence_selection,
selected);
(void) pspec;
if (state->updating_retained ||
selected == GTK_INVALID_LIST_POSITION || item == NULL) return;
person_evidence_selection_set_active(state->person_evidence_selection,
person_evidence_selection_item_get_identifier(item));
static const char *const types[] = {
"screenshot", "photo", "video", "document", "email",
"archive", "audio", "text", "other"};
for (guint i = 0; i < G_N_ELEMENTS(types); i++)
if (g_strcmp0(types[i],
person_evidence_selection_item_get_type_identifier(item)) == 0)
gtk_drop_down_set_selected(state->retained_type, i);
if (person_evidence_selection_item_get_origin(item) ==
PERSON_EVIDENCE_ORIGIN_EXISTING)
create_person_dialog_select_record(state,
person_evidence_selection_item_get_record(item),
person_evidence_selection_item_get_type_identifier(item));
else {
char *size = g_format_size(
person_evidence_selection_item_get_size_bytes(item));
char *text = g_strdup_printf(
"Nom : %s\nOrigine : nouvelle (staging)\nMIME : %s\n"
"Type : %s\nTaille : %s\nSHA-256 : %.12s…",
person_evidence_selection_item_get_original_name(item),
person_evidence_selection_item_get_mime_type(item),
person_evidence_selection_item_get_type_identifier(item), size,
person_evidence_selection_item_get_sha256(item));
create_person_dialog_clear_preview(state);
{
char *staging_root = g_path_get_dirname(
person_evidence_selection_item_get_staging_path(item));
char *staging_name = g_path_get_basename(
person_evidence_selection_item_get_staging_path(item));
EvidencePreviewRequest *request = evidence_preview_request_new(
staging_root,
person_evidence_selection_item_get_identifier(item),
staging_name,
person_evidence_selection_item_get_sha256(item),
person_evidence_selection_item_get_mime_type(item),
person_dialog_lifecycle_get_generation(state->lifecycle));
evidence_preview_widget_show(state->preview, request, text);
evidence_preview_request_free(request);
g_free(staging_root); g_free(staging_name);
}
g_free(text); g_free(size);
}
}
static void create_person_dialog_on_retained_type_changed(
GtkDropDown *dropdown, GParamSpec *pspec, gpointer data)
{
static const char *const types[] = {
"screenshot", "photo", "video", "document", "email",
"archive", "audio", "text", "other"};
CreatePersonDialogState *state = data;
guint selected = gtk_drop_down_get_selected(dropdown);
const PersonEvidenceSelectionItem *item =
person_evidence_selection_get_active(
state->person_evidence_selection);
(void) pspec;
if (item != NULL && selected < G_N_ELEMENTS(types)) {
person_evidence_selection_set_type(state->person_evidence_selection,
person_evidence_selection_item_get_identifier(item),
types[selected]);
create_person_dialog_rebuild_retained(state);
}
}
static void create_person_dialog_on_remove_retained(
GtkButton *button, gpointer data)
{
CreatePersonDialogState *state = data;
const PersonEvidenceSelectionItem *item =
person_evidence_selection_get_active(
state->person_evidence_selection);
char *identifier = item != NULL ? g_strdup(
person_evidence_selection_item_get_identifier(item)) : NULL;
char *staging_path = item != NULL &&
person_evidence_selection_item_get_origin(item) ==
PERSON_EVIDENCE_ORIGIN_STAGED
? g_strdup(person_evidence_selection_item_get_staging_path(item))
: NULL;
(void) button;
if (identifier != NULL) {
person_evidence_selection_remove(
state->person_evidence_selection, identifier);
if (staging_path != NULL)
evidence_staging_remove(state->staging, staging_path, NULL);
create_person_dialog_rebuild_retained(state);
create_person_dialog_clear_preview(state);
}
g_free(identifier); g_free(staging_path);
}
static void staging_context_free(gpointer data)
{
CreatePersonStagingContext *context = data;
if (context == NULL) return;
g_weak_ref_clear(&context->window);
g_free(context);
}
static void create_person_dialog_staging_completed(
BackgroundTask *task, gpointer data)
{
CreatePersonStagingContext *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;
GPtrArray *prepared = background_task_get_result(task);
if (state != NULL && state->session_check != NULL &&
!state->session_check(state->user_data)) {
person_dialog_lifecycle_cancel(state->lifecycle);
evidence_staging_cleanup(state->staging, NULL);
gtk_label_set_text(state->error,
"Préparation annulée : lenquête active a changé.");
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
} else if (state != NULL &&
person_dialog_lifecycle_accepts_preview(
state->lifecycle, context->generation) && prepared != NULL) {
for (guint i = 0; i < prepared->len; i++) {
EvidenceStagingResult *item = g_ptr_array_index(prepared, i);
const EvidenceRecord *existing =
evidence_selection_model_find_by_sha256(
state->selection_model, item->sha256);
GError *error = NULL;
if (existing != NULL) {
if (!person_evidence_selection_add_existing(
state->person_evidence_selection,
existing, &error) && error != NULL &&
error->code != 2)
gtk_label_set_text(state->error, error->message);
evidence_staging_remove(state->staging,
item->staging_path, NULL);
gtk_label_set_text(state->error,
"Doublon détecté : la preuve existante a été réutilisée.");
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
} else if (!person_evidence_selection_add_staged(
state->person_evidence_selection,
item->source_path, item->staging_path,
item->original_name, item->mime_type,
item->suggested_type, item->size_bytes, item->sha256,
NULL, item->prepared_at, &error)) {
evidence_staging_remove(state->staging,
item->staging_path, NULL);
gtk_label_set_text(state->error,
error != NULL ? error->message :
"Doublon refusé dans la sélection.");
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
}
g_clear_error(&error);
}
create_person_dialog_rebuild_retained(state);
} else if (state != NULL) {
GError *error = background_task_dup_error(task);
gtk_label_set_text(state->error,
error != NULL ? error->message :
"La préparation des preuves a échoué.");
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
g_clear_error(&error);
}
if (state != NULL && state->staging_task == task) {
background_task_unref(state->staging_task);
state->staging_task = NULL;
}
g_clear_object(&window);
}
static void create_person_dialog_files_selected(
GObject *source, GAsyncResult *async_result, gpointer data)
{
CreatePersonStagingContext *selection_context = data;
GtkWindow *window = g_weak_ref_get(&selection_context->window);
CreatePersonDialogState *state = window != NULL
? g_object_get_data(G_OBJECT(window), "person-dialog-state") : NULL;
GListModel *files;
GPtrArray *paths;
GError *error = NULL;
CreatePersonStagingContext *context;
files = gtk_file_dialog_open_multiple_finish(
GTK_FILE_DIALOG(source), async_result, &error);
if (state == NULL) {
g_clear_object(&files);
g_clear_error(&error);
staging_context_free(selection_context);
g_clear_object(&window);
return;
}
if (files == NULL) {
if (!g_error_matches(error, GTK_DIALOG_ERROR,
GTK_DIALOG_ERROR_DISMISSED)) {
gtk_label_set_text(state->error, error->message);
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
}
g_clear_error(&error);
staging_context_free(selection_context);
g_clear_object(&window);
return;
}
paths = g_ptr_array_new_with_free_func(g_free);
for (guint i = 0; i < g_list_model_get_n_items(files); i++) {
GFile *file = g_list_model_get_item(files, i);
char *path = g_file_get_path(file);
if (path != NULL) g_ptr_array_add(paths, path);
g_object_unref(file);
}
g_object_unref(files);
if (paths->len == 0) {
g_ptr_array_unref(paths);
staging_context_free(selection_context);
g_clear_object(&window);
return;
}
context = g_new0(CreatePersonStagingContext, 1);
g_weak_ref_init(&context->window, G_OBJECT(state->window));
context->generation =
person_dialog_lifecycle_begin_preview(state->lifecycle);
state->staging_task = evidence_staging_task_start(state->task_manager,
state->staging, paths, create_person_dialog_staging_completed,
context, staging_context_free, &error);
if (state->staging_task == NULL) {
staging_context_free(context);
gtk_label_set_text(state->error,
error != NULL ? error->message :
"Impossible de démarrer le staging.");
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
}
g_clear_error(&error);
g_ptr_array_unref(paths);
staging_context_free(selection_context);
g_clear_object(&window);
}
static void create_person_dialog_on_import_files(
GtkButton *button, gpointer data)
{
CreatePersonDialogState *state = data;
GtkFileDialog *dialog;
CreatePersonStagingContext *context;
(void) button;
if (state->staging_task != NULL) return;
dialog = gtk_file_dialog_new();
gtk_file_dialog_set_title(dialog,
"Importer des fichiers dans le staging");
context = g_new0(CreatePersonStagingContext, 1);
g_weak_ref_init(&context->window, G_OBJECT(state->window));
gtk_file_dialog_open_multiple(dialog, state->window, NULL,
create_person_dialog_files_selected, context);
g_object_unref(dialog);
}
static void create_person_dialog_rebuild_evidence(CreatePersonDialogState *state)
{
GtkStringList *labels = gtk_string_list_new(NULL);
GPtrArray *labels = g_ptr_array_new_with_free_func(g_free);
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_ptr_array_add(labels, g_strdup("Aucune preuve associée"));
g_ptr_array_set_size(state->evidence_identifiers, 0);
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));
g_ptr_array_add(labels,
g_strdup(evidence_record_get_original_name(record)));
g_ptr_array_add(state->evidence_identifiers,
g_strdup(evidence_record_get_identifier(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));
g_ptr_array_add(labels, NULL);
state->updating_evidence = TRUE;
if (state->evidence_selected_handler != 0)
g_signal_handler_block(state->evidence,
state->evidence_selected_handler);
gtk_string_list_splice(state->evidence_labels, 0,
g_list_model_get_n_items(G_LIST_MODEL(state->evidence_labels)),
(const char * const *) labels->pdata);
gtk_drop_down_set_selected(state->evidence, selected_index);
g_object_unref(labels);
if (state->evidence_selected_handler != 0)
g_signal_handler_unblock(state->evidence,
state->evidence_selected_handler);
state->updating_evidence = FALSE;
g_ptr_array_unref(labels);
if (selected_index == 0) {
evidence_selection_model_select(state->selection_model, NULL);
create_person_dialog_clear_preview(state);
@ -160,29 +525,28 @@ static void create_person_dialog_on_evidence_changed(
{
CreatePersonDialogState *state = data;
guint selected = gtk_drop_down_get_selected(dropdown);
const char *identifier = NULL;
const EvidenceRecord *record = NULL;
(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);
if (state->updating_evidence) return;
if (selected != GTK_INVALID_LIST_POSITION && selected > 0 &&
selected - 1 < state->evidence_identifiers->len)
identifier = g_ptr_array_index(
state->evidence_identifiers, selected - 1);
if (identifier != NULL)
record = evidence_selection_model_find_by_identifier(
state->selection_model, identifier);
create_person_dialog_select_record(state, record, NULL);
}
/** @brief Termine le dialogue comme une annulation. */
static void create_person_dialog_cancel(CreatePersonDialogState *state)
{
if (state == NULL ||
!person_dialog_lifecycle_cancel(state->lifecycle)) return;
if (state->preview_task != NULL) background_task_cancel(state->preview_task);
evidence_preview_widget_cancel(state->preview);
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) {
@ -196,72 +560,17 @@ static const char *integrity_label(EvidenceIntegrityStatus status)
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);
evidence_preview_widget_clear(state->preview);
}
static void create_person_dialog_select_record(CreatePersonDialogState *state,
const EvidenceRecord *record)
const EvidenceRecord *record, const char *business_type)
{
char *size = NULL, *sha = NULL, *text = NULL;
const char *mime;
CreatePersonPreviewContext *context;
const char *type_identifier;
EvidencePreviewRequest *request;
GError *error = NULL;
create_person_dialog_clear_preview(state);
if (record == NULL) return;
evidence_selection_model_select(state->selection_model,
@ -269,48 +578,25 @@ static void create_person_dialog_select_record(CreatePersonDialogState *state,
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);
type_identifier = business_type != NULL ? business_type :
evidence_record_get_type_identifier(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),
type_identifier, type_identifier,
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_widget_show(state->preview, request, text);
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. */
@ -331,8 +617,6 @@ static void create_person_dialog_on_create(GtkButton *button, gpointer data)
CreatePersonDialogResult *result = NULL;
char *notes = NULL;
guint status = gtk_drop_down_get_selected(state->status);
const EvidenceRecord *selected_record =
evidence_selection_model_get_selected(state->selection_model);
(void) button;
if (state->session_check != NULL &&
!state->session_check(state->user_data)) {
@ -357,9 +641,14 @@ static void create_person_dialog_on_create(GtkButton *button, gpointer data)
gtk_editable_get_text(GTK_EDITABLE(state->pseudonym)));
result->status = g_strdup(status_codes[status]);
result->notes = create_person_dialog_copy(notes);
if (selected_record != NULL)
if (person_evidence_selection_get_count(
state->person_evidence_selection) > 0) {
const PersonEvidenceSelectionItem *first =
person_evidence_selection_get(
state->person_evidence_selection, 0);
result->evidence_identifier = g_strdup(
evidence_record_get_identifier(selected_record));
person_evidence_selection_item_get_evidence_identifier(first));
}
result->input.designation = result->designation;
result->input.declared_name = result->declared_name;
result->input.pseudonym = result->pseudonym;
@ -385,6 +674,10 @@ static void create_person_dialog_on_create(GtkButton *button, gpointer data)
person_role_assignment_input_copy(&assignment));
}
result->input.role_assignments = result->role_assignments;
result->evidence_selection = state->person_evidence_selection;
state->person_evidence_selection = NULL;
result->staging = state->staging;
state->staging = NULL;
if (!person_dialog_lifecycle_complete(state->lifecycle)) {
create_person_dialog_result_free(result);
g_free(notes);
@ -399,7 +692,7 @@ static void create_person_dialog_update_navigation(CreatePersonDialogState *stat
{
static const char *const pages[] = {"person", "roles", "evidence", "summary"};
static const char *const steps[] = {
"1 Personne", "2 Rôles", "3 Preuve", "4 Confirmation"};
"1 Personne", "2 Rôles", "3 Preuves", "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++) {
@ -417,15 +710,13 @@ static void create_person_dialog_update_navigation(CreatePersonDialogState *stat
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(
text = person_confirmation_summary_build_multiple(
gtk_editable_get_text(GTK_EDITABLE(state->designation)),
gtk_editable_get_text(GTK_EDITABLE(state->name)),
gtk_editable_get_text(GTK_EDITABLE(state->pseudonym)),
@ -434,7 +725,7 @@ static void create_person_dialog_update_navigation(CreatePersonDialogState *stat
? status_labels[gtk_drop_down_get_selected(state->status)]
: "Non renseigné",
gtk_spin_button_get_value_as_int(state->confidence), notes,
role_labels, evidence);
role_labels, state->person_evidence_selection);
gtk_label_set_text(state->summary, text);
g_free(text);
g_free(notes);
@ -443,6 +734,10 @@ static void create_person_dialog_update_navigation(CreatePersonDialogState *stat
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);
gtk_widget_set_sensitive(GTK_WIDGET(state->create),
state->step == 3 &&
person_evidence_selection_is_confirmable(
state->person_evidence_selection));
}
static void create_person_dialog_on_previous(GtkButton *button, gpointer data)
{
@ -484,7 +779,6 @@ gboolean create_person_dialog_present(GtkWindow *parent,
CreatePersonDialogState *state = NULL;
GtkWidget *box = NULL, *grid = NULL, *actions = NULL, *cancel = NULL;
GtkWidget *roles_box = NULL, *evidence_box = NULL, *summary = NULL;
GtkStringList *labels = NULL, *type_labels = NULL;
if (parent == NULL || investigation_root_path == NULL ||
task_manager == NULL) return FALSE;
state = g_new0(CreatePersonDialogState, 1);
@ -492,6 +786,13 @@ gboolean create_person_dialog_present(GtkWindow *parent,
state->user_data = data; state->user_data_destroy = data_destroy;
state->evidence_identifiers = g_ptr_array_new_with_free_func(g_free);
state->selection_model = evidence_selection_model_new(records);
state->person_evidence_selection = person_evidence_selection_new();
state->staging = evidence_staging_new(NULL);
if (state->selection_model == NULL ||
state->person_evidence_selection == NULL || state->staging == NULL) {
create_person_dialog_state_free(state);
return FALSE;
}
state->visible_records =
evidence_selection_model_list_visible(state->selection_model);
state->type_codes =
@ -505,7 +806,7 @@ gboolean create_person_dialog_present(GtkWindow *parent,
gtk_window_set_title(state->window, "Ajouter une personne");
gtk_window_set_transient_for(state->window, parent);
gtk_window_set_modal(state->window, TRUE);
gtk_window_set_default_size(state->window, 580, 500);
gtk_window_set_default_size(state->window, 820, 680);
box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
gtk_widget_set_margin_start(box, 16); gtk_widget_set_margin_end(box, 16);
gtk_widget_set_margin_top(box, 16); gtk_widget_set_margin_bottom(box, 16);
@ -519,35 +820,36 @@ gboolean create_person_dialog_present(GtkWindow *parent,
gtk_drop_down_set_selected(state->status, 1);
state->confidence = GTK_SPIN_BUTTON(gtk_spin_button_new_with_range(0, 100, 5));
gtk_spin_button_set_value(state->confidence, 30);
labels = gtk_string_list_new(NULL);
gtk_string_list_append(labels, "Aucune preuve associée");
state->evidence_labels = gtk_string_list_new(NULL);
gtk_string_list_append(state->evidence_labels,
"Aucune preuve associée");
for (guint i = 0; i < state->visible_records->len; i++)
{
EvidenceRecord *record = g_ptr_array_index(state->visible_records, i);
const char *id = evidence_record_get_identifier(record);
const char *name = evidence_record_get_original_name(record);
if (id == NULL || name == NULL) continue;
gtk_string_list_append(labels, name);
gtk_string_list_append(state->evidence_labels, name);
g_ptr_array_add(state->evidence_identifiers, g_strdup(id));
}
state->evidence = GTK_DROP_DOWN(gtk_drop_down_new(G_LIST_MODEL(labels), NULL));
labels = NULL;
state->evidence = GTK_DROP_DOWN(gtk_drop_down_new(
G_LIST_MODEL(g_object_ref(state->evidence_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");
state->type_filter_labels = gtk_string_list_new(NULL);
gtk_string_list_append(state->type_filter_labels, "Tous les types");
for (guint i = 0; i < state->type_codes->len; i++)
gtk_string_list_append(type_labels,
gtk_string_list_append(state->type_filter_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(G_LIST_MODEL(
g_object_ref(state->type_filter_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());
gtk_text_view_set_wrap_mode(state->notes, GTK_WRAP_WORD_CHAR);
gtk_widget_set_size_request(GTK_WIDGET(state->notes), -1, 90);
@ -574,24 +876,63 @@ gboolean create_person_dialog_present(GtkWindow *parent,
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)."));
"Sélectionnez des preuves existantes et/ou préparez de nouveaux fichiers."));
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");
{
GtkWidget *evidence_actions =
gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
state->add_existing = GTK_BUTTON(
gtk_button_new_with_label("Ajouter à la sélection"));
state->import_files = GTK_BUTTON(
gtk_button_new_with_label("Importer des fichiers"));
gtk_widget_set_tooltip_text(GTK_WIDGET(state->add_existing),
"Retenir la preuve existante affichée");
gtk_widget_set_tooltip_text(GTK_WIDGET(state->import_files),
"Créer des copies temporaires sans import définitif");
gtk_box_append(GTK_BOX(evidence_actions),
GTK_WIDGET(state->add_existing));
gtk_box_append(GTK_BOX(evidence_actions),
GTK_WIDGET(state->import_files));
gtk_box_append(GTK_BOX(evidence_box), evidence_actions);
}
gtk_box_append(GTK_BOX(evidence_box),
gtk_label_new("Preuves retenues"));
{
state->retained_labels = gtk_string_list_new(NULL);
state->retained = GTK_DROP_DOWN(gtk_drop_down_new(
G_LIST_MODEL(g_object_ref(state->retained_labels)), NULL));
}
gtk_box_append(GTK_BOX(evidence_box), GTK_WIDGET(state->retained));
{
static const char *const type_labels_fr[] = {
"Capture décran", "Photo", "Vidéo", "Document", "E-mail",
"Archive", "Audio", "Texte", "Autre", NULL};
GtkWidget *retained_actions =
gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
state->retained_type = GTK_DROP_DOWN(
gtk_drop_down_new_from_strings(type_labels_fr));
state->remove_retained = GTK_BUTTON(
gtk_button_new_with_label("Retirer"));
gtk_widget_set_tooltip_text(GTK_WIDGET(state->remove_retained),
"Retirer uniquement de la sélection");
gtk_box_append(GTK_BOX(retained_actions),
GTK_WIDGET(state->retained_type));
gtk_box_append(GTK_BOX(retained_actions),
GTK_WIDGET(state->remove_retained));
gtk_box_append(GTK_BOX(evidence_box), retained_actions);
}
state->preview = evidence_preview_widget_new(state->task_manager,
(EvidencePreviewWidgetSessionCheck) state->session_check,
state->user_data);
if (state->preview == NULL) {
create_person_dialog_state_free(state);
return FALSE;
}
gtk_box_append(GTK_BOX(evidence_box),
evidence_preview_widget_get_widget(state->preview));
gtk_stack_add_titled(state->stack, evidence_box, "evidence", "3 — Preuves");
state->summary = GTK_LABEL(gtk_label_new(""));
summary = GTK_WIDGET(state->summary);
gtk_label_set_wrap(state->summary, TRUE);
@ -627,8 +968,20 @@ gboolean create_person_dialog_present(GtkWindow *parent,
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",
state->evidence_selected_handler = g_signal_connect(
state->evidence, "notify::selected",
G_CALLBACK(create_person_dialog_on_evidence_changed), state);
g_signal_connect(state->add_existing, "clicked",
G_CALLBACK(create_person_dialog_on_add_existing), state);
state->retained_selected_handler = g_signal_connect(
state->retained, "notify::selected",
G_CALLBACK(create_person_dialog_on_retained_changed), state);
g_signal_connect(state->retained_type, "notify::selected",
G_CALLBACK(create_person_dialog_on_retained_type_changed), state);
g_signal_connect(state->remove_retained, "clicked",
G_CALLBACK(create_person_dialog_on_remove_retained), state);
g_signal_connect(state->import_files, "clicked",
G_CALLBACK(create_person_dialog_on_import_files), state);
create_person_dialog_update_navigation(state);
g_object_set_data_full(G_OBJECT(state->window), "person-dialog-state", state,
create_person_dialog_state_free);
@ -641,6 +994,9 @@ void create_person_dialog_result_free(CreatePersonDialogResult *result)
g_free(result->pseudonym); g_free(result->status); g_free(result->notes);
g_free(result->evidence_identifier);
g_clear_pointer(&result->role_assignments, g_ptr_array_unref);
g_clear_pointer(&result->evidence_selection,
person_evidence_selection_free);
g_clear_pointer(&result->staging, evidence_staging_free);
g_free(result);
}
const PersonEntityInput *create_person_dialog_result_get_input(
@ -648,3 +1004,16 @@ const PersonEntityInput *create_person_dialog_result_get_input(
{
return result != NULL ? &result->input : NULL;
}
const PersonEvidenceSelection *
create_person_dialog_result_get_evidence_selection(
const CreatePersonDialogResult *result)
{
return result != NULL ? result->evidence_selection : NULL;
}
EvidenceStaging *create_person_dialog_result_steal_staging(
CreatePersonDialogResult *result)
{
EvidenceStaging *staging = result != NULL ? result->staging : NULL;
if (result != NULL) result->staging = NULL;
return staging;
}

View file

@ -958,7 +958,7 @@ MainWindow *main_window_new(
return NULL;
}
main_window->workspace = workspace_new();
main_window->workspace = workspace_new(task_manager);
if (main_window->workspace == NULL)
{
@ -2028,6 +2028,14 @@ void main_window_set_evidence_preview(MainWindow *main_window,
display_name);
}
void main_window_show_evidence_preview(MainWindow *main_window,
const char *investigation_root, const EvidenceRecord *record)
{
if (main_window == NULL) return;
workspace_show_evidence_preview(main_window->workspace,
investigation_root, record);
}
void main_window_free(
MainWindow *main_window
)

View file

@ -0,0 +1,346 @@
#include "widgets/evidence_preview_widget.h"
#include "core/evidence_preview_task.h"
#include "core/evidence_video_preview_controller.h"
struct EvidencePreviewWidget {
GtkWidget *root;
GtkLabel *metadata;
GtkLabel *status;
GtkStack *stack;
GtkPicture *image;
GtkPicture *pdf;
GtkVideo *video;
GtkTextView *text;
GtkTextView *email;
TaskManager *task_manager;
BackgroundTask *task;
EvidenceVideoPreviewController *video_controller;
EvidencePreviewWidgetSessionCheck session_check;
gpointer session_data;
guint64 generation;
char *identifier;
};
typedef struct {
GWeakRef root;
char *identifier;
guint64 generation;
} PreviewWidgetContext;
static void preview_widget_root_finalized(gpointer data, GObject *object)
{
EvidencePreviewWidget *widget = data;
(void) object;
if (widget != NULL) widget->root = NULL;
}
static void preview_widget_media_pause(gpointer media)
{ gtk_media_stream_pause(GTK_MEDIA_STREAM(media)); }
static void preview_widget_media_seek(gpointer media)
{ gtk_media_stream_seek(GTK_MEDIA_STREAM(media), 0); }
static void preview_widget_media_detach(gpointer owner)
{ gtk_video_set_media_stream(GTK_VIDEO(owner), NULL); }
static void preview_widget_media_release(gpointer media)
{ g_object_unref(media); }
static void preview_widget_context_free(gpointer data)
{
PreviewWidgetContext *context = data;
if (context == NULL) return;
g_weak_ref_clear(&context->root);
g_free(context->identifier);
g_free(context);
}
static void preview_widget_media_error(GtkMediaStream *media,
GParamSpec *pspec, gpointer data)
{
EvidencePreviewWidget *widget = data;
const GError *error;
(void) pspec;
if (widget == NULL || widget->root == NULL) return;
error = gtk_media_stream_get_error(media);
if (error != NULL)
gtk_label_set_text(widget->status,
"Vidéo reconnue, mais codec de lecture indisponible.");
}
static void preview_widget_reset_content(EvidencePreviewWidget *widget)
{
if (widget->video != NULL)
evidence_video_preview_controller_stop(widget->video_controller);
if (widget->image != NULL) gtk_picture_set_paintable(widget->image, NULL);
if (widget->pdf != NULL) gtk_picture_set_paintable(widget->pdf, NULL);
if (widget->text != NULL)
gtk_text_buffer_set_text(gtk_text_view_get_buffer(widget->text), "", -1);
if (widget->email != NULL)
gtk_text_buffer_set_text(gtk_text_view_get_buffer(widget->email), "", -1);
}
void evidence_preview_widget_cancel(EvidencePreviewWidget *widget)
{
if (widget == NULL) return;
widget->generation++;
if (widget->task != NULL) {
background_task_cancel(widget->task);
background_task_unref(widget->task);
widget->task = NULL;
}
if (widget->root != NULL)
preview_widget_reset_content(widget);
g_clear_pointer(&widget->identifier, g_free);
}
void evidence_preview_widget_clear(EvidencePreviewWidget *widget)
{
if (widget == NULL) return;
evidence_preview_widget_cancel(widget);
gtk_label_set_text(widget->metadata, "");
gtk_label_set_text(widget->status, "Aucune preuve sélectionnée.");
gtk_stack_set_visible_child_name(widget->stack, "empty");
}
static void preview_widget_completed(BackgroundTask *task, gpointer data)
{
PreviewWidgetContext *context = data;
GtkWidget *root = GTK_WIDGET(g_weak_ref_get(&context->root));
EvidencePreviewWidget *widget = root != NULL
? g_object_get_data(G_OBJECT(root), "evidence-preview-widget") : NULL;
EvidencePreviewResult *result = background_task_get_result(task);
gboolean current = widget != NULL &&
widget->generation == context->generation &&
g_strcmp0(widget->identifier, context->identifier) == 0 &&
(widget->session_check == NULL ||
widget->session_check(widget->session_data));
if (current && evidence_preview_result_matches(result,
context->identifier, context->generation)) {
GError *error = NULL;
if ((result->kind == EVIDENCE_PREVIEW_KIND_IMAGE ||
result->kind == EVIDENCE_PREVIEW_KIND_PDF) &&
result->png_bytes != NULL) {
GdkTexture *texture = gdk_texture_new_from_bytes(
result->png_bytes, &error);
if (texture != NULL) {
GtkPicture *picture = result->kind == EVIDENCE_PREVIEW_KIND_PDF
? widget->pdf : widget->image;
gtk_picture_set_paintable(picture, GDK_PAINTABLE(texture));
gtk_stack_set_visible_child_name(widget->stack,
result->kind == EVIDENCE_PREVIEW_KIND_PDF
? "pdf" : "image");
gtk_label_set_text(widget->status, "Aperçu intègre.");
g_object_unref(texture);
}
} else if (result->kind == EVIDENCE_PREVIEW_KIND_VIDEO) {
GtkMediaStream *media = gtk_media_file_new_for_filename(
result->controlled_path);
gtk_media_stream_set_volume(media, 0.0);
gtk_media_stream_pause(media);
g_signal_connect(media, "notify::error",
G_CALLBACK(preview_widget_media_error), widget);
gtk_video_set_autoplay(widget->video, FALSE);
gtk_video_set_media_stream(widget->video, media);
evidence_video_preview_controller_replace(
widget->video_controller, media);
gtk_stack_set_visible_child_name(widget->stack, "video");
gtk_label_set_text(widget->status, result->message);
} else if (result->kind == EVIDENCE_PREVIEW_KIND_TEXT ||
result->kind == EVIDENCE_PREVIEW_KIND_EMAIL) {
GtkTextView *view = result->kind == EVIDENCE_PREVIEW_KIND_EMAIL
? widget->email : widget->text;
gtk_text_buffer_set_text(gtk_text_view_get_buffer(view),
result->text != NULL ? result->text : "", -1);
gtk_stack_set_visible_child_name(widget->stack,
result->kind == EVIDENCE_PREVIEW_KIND_EMAIL
? "email" : "text");
gtk_label_set_text(widget->status,
result->truncated ? "Aperçu tronqué." : "Aperçu intègre.");
} else {
gtk_stack_set_visible_child_name(widget->stack,
result->kind == EVIDENCE_PREVIEW_KIND_UNSUPPORTED
? "unsupported" : "error");
gtk_label_set_text(widget->status,
result->message != NULL ? result->message :
"Aperçu illisible.");
}
if (error != NULL) {
gtk_stack_set_visible_child_name(widget->stack, "error");
gtk_label_set_text(widget->status, error->message);
}
g_clear_error(&error);
} else if (widget != NULL && widget->generation == context->generation) {
GError *error = background_task_dup_error(task);
gtk_stack_set_visible_child_name(widget->stack, "error");
gtk_label_set_text(widget->status,
error != NULL ? error->message :
"Aperçu annulé : lenquête active a changé.");
g_clear_error(&error);
}
if (widget != NULL && widget->task == task) {
background_task_unref(widget->task);
widget->task = NULL;
}
g_clear_object(&root);
}
EvidencePreviewWidget *evidence_preview_widget_new(
TaskManager *task_manager,
EvidencePreviewWidgetSessionCheck session_check,
gpointer session_data)
{
EvidencePreviewWidget *widget;
GtkWidget *text_scroll;
GtkWidget *email_scroll;
const EvidenceVideoPreviewActions actions = {
preview_widget_media_pause, preview_widget_media_seek,
preview_widget_media_detach, preview_widget_media_release
};
if (task_manager == NULL) return NULL;
widget = g_new0(EvidencePreviewWidget, 1);
widget->task_manager = task_manager;
widget->session_check = session_check;
widget->session_data = session_data;
widget->root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
g_object_weak_ref(G_OBJECT(widget->root),
preview_widget_root_finalized, widget);
widget->metadata = GTK_LABEL(gtk_label_new(""));
gtk_label_set_xalign(widget->metadata, 0.0f);
gtk_label_set_selectable(widget->metadata, TRUE);
widget->status = GTK_LABEL(gtk_label_new("Aucune preuve sélectionnée."));
gtk_label_set_xalign(widget->status, 0.0f);
widget->image = GTK_PICTURE(gtk_picture_new());
widget->pdf = GTK_PICTURE(gtk_picture_new());
widget->video = GTK_VIDEO(gtk_video_new());
g_object_add_weak_pointer(G_OBJECT(widget->image),
(gpointer *) &widget->image);
g_object_add_weak_pointer(G_OBJECT(widget->pdf),
(gpointer *) &widget->pdf);
g_object_add_weak_pointer(G_OBJECT(widget->video),
(gpointer *) &widget->video);
for (guint i = 0; i < 2; i++) {
GtkPicture *picture = i == 0 ? widget->image : widget->pdf;
gtk_picture_set_can_shrink(picture, TRUE);
gtk_picture_set_content_fit(picture, GTK_CONTENT_FIT_CONTAIN);
gtk_widget_set_size_request(GTK_WIDGET(picture), 480, 260);
}
gtk_widget_set_size_request(GTK_WIDGET(widget->video), 480, 260);
gtk_video_set_autoplay(widget->video, FALSE);
widget->video_controller = evidence_video_preview_controller_new(
widget->video, &actions);
widget->text = GTK_TEXT_VIEW(gtk_text_view_new());
widget->email = GTK_TEXT_VIEW(gtk_text_view_new());
g_object_add_weak_pointer(G_OBJECT(widget->text),
(gpointer *) &widget->text);
g_object_add_weak_pointer(G_OBJECT(widget->email),
(gpointer *) &widget->email);
gtk_text_view_set_monospace(widget->text, TRUE);
for (guint i = 0; i < 2; i++) {
GtkTextView *view = i == 0 ? widget->text : widget->email;
gtk_text_view_set_editable(view, FALSE);
gtk_text_view_set_cursor_visible(view, FALSE);
gtk_text_view_set_wrap_mode(view, GTK_WRAP_WORD_CHAR);
}
text_scroll = gtk_scrolled_window_new();
email_scroll = gtk_scrolled_window_new();
gtk_widget_set_size_request(text_scroll, 480, 260);
gtk_widget_set_size_request(email_scroll, 480, 260);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(text_scroll),
GTK_WIDGET(widget->text));
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(email_scroll),
GTK_WIDGET(widget->email));
widget->stack = GTK_STACK(gtk_stack_new());
gtk_stack_add_named(widget->stack, gtk_label_new("Aucun aperçu."), "empty");
gtk_stack_add_named(widget->stack,
gtk_label_new("Chargement de laperçu…"), "loading");
gtk_stack_add_named(widget->stack, GTK_WIDGET(widget->image), "image");
gtk_stack_add_named(widget->stack, GTK_WIDGET(widget->video), "video");
gtk_stack_add_named(widget->stack, text_scroll, "text");
gtk_stack_add_named(widget->stack, email_scroll, "email");
gtk_stack_add_named(widget->stack, GTK_WIDGET(widget->pdf), "pdf");
gtk_stack_add_named(widget->stack,
gtk_label_new("Format reconnu mais aperçu indisponible."),
"unsupported");
gtk_stack_add_named(widget->stack,
gtk_label_new("Erreur pendant la préparation de laperçu."), "error");
gtk_stack_set_visible_child_name(widget->stack, "empty");
gtk_box_append(GTK_BOX(widget->root), GTK_WIDGET(widget->metadata));
gtk_box_append(GTK_BOX(widget->root), GTK_WIDGET(widget->status));
gtk_box_append(GTK_BOX(widget->root), GTK_WIDGET(widget->stack));
g_object_set_data(G_OBJECT(widget->root),
"evidence-preview-widget", widget);
return widget;
}
GtkWidget *evidence_preview_widget_get_widget(
const EvidencePreviewWidget *widget)
{ return widget != NULL ? widget->root : NULL; }
void evidence_preview_widget_show(EvidencePreviewWidget *widget,
const EvidencePreviewRequest *request, const char *metadata)
{
EvidencePreviewRequest *owned;
PreviewWidgetContext *context;
GError *error = NULL;
if (widget == NULL || request == NULL) return;
evidence_preview_widget_cancel(widget);
widget->identifier = g_strdup(request->evidence_identifier);
owned = evidence_preview_request_new(request->investigation_root_path,
request->evidence_identifier, request->relative_path,
request->expected_sha256, request->mime_type, widget->generation);
gtk_label_set_text(widget->metadata, metadata != NULL ? metadata : "");
gtk_label_set_text(widget->status,
"Détection, vérification et chargement en cours…");
gtk_stack_set_visible_child_name(widget->stack, "loading");
context = g_new0(PreviewWidgetContext, 1);
g_weak_ref_init(&context->root, G_OBJECT(widget->root));
context->identifier = g_strdup(widget->identifier);
context->generation = widget->generation;
if (owned != NULL)
widget->task = evidence_preview_task_start(widget->task_manager,
owned, preview_widget_completed, context,
preview_widget_context_free, &error);
evidence_preview_request_free(owned);
if (widget->task == NULL) {
preview_widget_context_free(context);
gtk_stack_set_visible_child_name(widget->stack, "error");
gtk_label_set_text(widget->status,
error != NULL ? error->message : "Aperçu impossible.");
}
g_clear_error(&error);
}
void evidence_preview_widget_free(EvidencePreviewWidget *widget)
{
if (widget == NULL) return;
evidence_preview_widget_cancel(widget);
if (widget->root != NULL) {
g_object_set_data(G_OBJECT(widget->root),
"evidence-preview-widget", NULL);
g_object_weak_unref(G_OBJECT(widget->root),
preview_widget_root_finalized, widget);
}
if (widget->video == NULL)
evidence_video_preview_controller_abandon_owner(
widget->video_controller);
g_clear_pointer(&widget->video_controller,
evidence_video_preview_controller_free);
g_free(widget);
}
const char *evidence_preview_widget_get_state(
const EvidencePreviewWidget *widget)
{
return widget != NULL
? gtk_stack_get_visible_child_name(widget->stack) : NULL;
}
char *evidence_preview_widget_dup_text(
const EvidencePreviewWidget *widget)
{
GtkTextBuffer *buffer;
GtkTextIter start;
GtkTextIter end;
if (widget == NULL) return NULL;
buffer = gtk_text_view_get_buffer(widget->email);
gtk_text_buffer_get_bounds(buffer, &start, &end);
return gtk_text_buffer_get_text(buffer, &start, &end, FALSE);
}

View file

@ -12,6 +12,7 @@
#include "models/relation_record.h"
#include "widgets/entity_details_panel.h"
#include "widgets/investigation_graph_view.h"
#include "widgets/evidence_preview_widget.h"
#include <glib.h>
#include <gio/gio.h>
@ -97,14 +98,7 @@ struct Workspace
GtkWidget *analyze_rib_button;
GtkWidget *extract_metadata_button;
GtkWidget *recover_pdf_password_button;
GtkWidget *evidence_preview_stack;
GtkWidget *evidence_preview_status;
GtkWidget *evidence_preview_text;
GtkWidget *evidence_preview_picture;
GtkWidget *evidence_preview_video;
GtkWidget *evidence_preview_expand_button;
char *evidence_preview_path;
gboolean evidence_preview_is_video;
EvidencePreviewWidget *evidence_preview;
char *selected_evidence_identifier;
@ -639,111 +633,11 @@ static const char *workspace_get_integrity_status_text(
}
}
/** @brief Affiche une erreur asynchrone produite par le moteur vidéo. */
static void workspace_on_video_error_changed(GObject *object,
GParamSpec *parameter, gpointer user_data)
{
Workspace *workspace = user_data;
const GError *error = NULL;
(void) parameter;
if (workspace == NULL || !GTK_IS_MEDIA_STREAM(object)) return;
error = gtk_media_stream_get_error(GTK_MEDIA_STREAM(object));
if (error == NULL) return;
gtk_label_set_text(GTK_LABEL(workspace->evidence_preview_status),
error->message != NULL ? error->message :
"Le moteur vidéo ne peut pas lire ce fichier.");
gtk_stack_set_visible_child_name(GTK_STACK(
workspace->evidence_preview_stack), "status");
gtk_widget_set_sensitive(workspace->evidence_preview_expand_button,
FALSE);
}
/** @brief Arrête et détache le média vidéo actuellement affiché. */
static void workspace_clear_evidence_preview(Workspace *workspace)
{
GtkMediaStream *stream = NULL;
if (workspace == NULL) return;
if (workspace->evidence_preview_video != NULL)
{
stream = gtk_video_get_media_stream(
GTK_VIDEO(workspace->evidence_preview_video));
if (stream != NULL)
{
g_signal_handlers_disconnect_by_data(stream, workspace);
gtk_media_stream_pause(stream);
}
gtk_video_set_media_stream(GTK_VIDEO(
workspace->evidence_preview_video), NULL);
}
if (workspace->evidence_preview_picture != NULL)
gtk_picture_set_paintable(GTK_PICTURE(
workspace->evidence_preview_picture), NULL);
g_clear_pointer(&workspace->evidence_preview_path, g_free);
workspace->evidence_preview_is_video = FALSE;
if (workspace->evidence_preview_expand_button != NULL)
gtk_widget_set_sensitive(workspace->evidence_preview_expand_button,
FALSE);
}
/** @brief Ouvre le média courant dans une fenêtre d'aperçu agrandie. */
static void workspace_on_expand_evidence_preview(GtkButton *button,
gpointer user_data)
{
Workspace *workspace = user_data;
GtkWindow *window = NULL;
GtkWidget *content = NULL;
(void) button;
if (workspace == NULL || workspace->evidence_preview_path == NULL) return;
window = GTK_WINDOW(gtk_window_new());
gtk_window_set_title(window, "Aperçu de la preuve");
gtk_window_set_default_size(window, 960, 720);
if (workspace->evidence_preview_is_video)
{
content = gtk_video_new_for_filename(workspace->evidence_preview_path);
gtk_video_set_autoplay(GTK_VIDEO(content), FALSE);
}
else if (gtk_stack_get_visible_child(GTK_STACK(
workspace->evidence_preview_stack)) ==
workspace->evidence_preview_status)
{
GFile *file = g_file_new_for_path(workspace->evidence_preview_path);
char *uri = g_file_get_uri(file);
GError *error = NULL;
if (uri == NULL || !g_app_info_launch_default_for_uri(uri, NULL,
&error))
{
gtk_label_set_text(GTK_LABEL(workspace->evidence_preview_status),
error != NULL ? error->message :
"Impossible douvrir ce document.");
g_clear_error(&error);
}
g_free(uri);
g_object_unref(file);
gtk_window_destroy(window);
return;
}
else
{
content = gtk_picture_new_for_filename(workspace->evidence_preview_path);
gtk_picture_set_can_shrink(GTK_PICTURE(content), TRUE);
gtk_picture_set_content_fit(GTK_PICTURE(content), GTK_CONTENT_FIT_CONTAIN);
}
gtk_window_set_child(window, content);
gtk_window_present(window);
}
/** @brief Ouvre laperçu agrandi lorsquun utilisateur clique dessus. */
static void workspace_on_evidence_preview_clicked(GtkGestureClick *gesture,
gint n_press, gdouble x, gdouble y, gpointer user_data)
{
Workspace *workspace = user_data;
(void) gesture;
(void) x;
(void) y;
if (workspace == NULL || n_press != 1 ||
workspace->evidence_preview_path == NULL)
return;
workspace_on_expand_evidence_preview(NULL, workspace);
evidence_preview_widget_clear(workspace->evidence_preview);
}
/** @brief Convertit le statut d'une relation en texte utilisateur. */
@ -1212,7 +1106,7 @@ static void workspace_on_recover_pdf_password_clicked(GtkButton *button,
workspace->recover_pdf_password_user_data);
}
Workspace *workspace_new(void)
Workspace *workspace_new(TaskManager *task_manager)
{
GtkWidget *evidence_content = NULL;
GtkWidget *evidence_separator = NULL;
@ -1220,6 +1114,7 @@ Workspace *workspace_new(void)
Workspace *workspace = NULL;
if (task_manager == NULL) return NULL;
workspace = g_new0(Workspace, 1);
workspace->root_widget = gtk_box_new(
@ -1485,65 +1380,14 @@ Workspace *workspace_new(void)
workspace->evidence_name_label
);
workspace->evidence_preview_stack = gtk_stack_new();
workspace->evidence_preview_status = gtk_label_new(
"Aucun aperçu disponible pour cette preuve.");
workspace->evidence_preview_picture = gtk_picture_new();
workspace->evidence_preview_video = gtk_video_new();
workspace->evidence_preview_expand_button =
gtk_button_new_with_label("Agrandir laperçu");
gtk_widget_set_size_request(workspace->evidence_preview_stack, -1, 180);
gtk_widget_set_vexpand(workspace->evidence_preview_stack, FALSE);
gtk_widget_set_hexpand(workspace->evidence_preview_stack, TRUE);
gtk_widget_set_size_request(workspace->evidence_preview_picture, -1, 180);
gtk_widget_set_size_request(workspace->evidence_preview_video, -1, 180);
gtk_widget_set_vexpand(workspace->evidence_preview_picture, FALSE);
gtk_widget_set_vexpand(workspace->evidence_preview_video, FALSE);
gtk_picture_set_can_shrink(GTK_PICTURE(
workspace->evidence_preview_picture), TRUE);
gtk_picture_set_content_fit(GTK_PICTURE(
workspace->evidence_preview_picture), GTK_CONTENT_FIT_CONTAIN);
gtk_video_set_autoplay(GTK_VIDEO(workspace->evidence_preview_video), FALSE);
gtk_stack_add_named(GTK_STACK(workspace->evidence_preview_stack),
workspace->evidence_preview_status, "status");
workspace->evidence_preview_text = gtk_text_view_new();
gtk_text_view_set_editable(GTK_TEXT_VIEW(workspace->evidence_preview_text), FALSE);
gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(workspace->evidence_preview_text), GTK_WRAP_WORD_CHAR);
gtk_widget_set_vexpand(workspace->evidence_preview_text, TRUE);
gtk_stack_add_named(GTK_STACK(workspace->evidence_preview_stack),
workspace->evidence_preview_text, "text");
gtk_stack_add_named(GTK_STACK(workspace->evidence_preview_stack),
workspace->evidence_preview_picture, "image");
gtk_stack_add_named(GTK_STACK(workspace->evidence_preview_stack),
workspace->evidence_preview_video, "video");
{
GtkGesture *gesture = gtk_gesture_click_new();
gtk_widget_add_controller(workspace->evidence_preview_picture,
GTK_EVENT_CONTROLLER(gesture));
g_signal_connect(gesture, "released",
G_CALLBACK(workspace_on_evidence_preview_clicked), workspace);
gesture = gtk_gesture_click_new();
gtk_widget_add_controller(workspace->evidence_preview_video,
GTK_EVENT_CONTROLLER(gesture));
g_signal_connect(gesture, "released",
G_CALLBACK(workspace_on_evidence_preview_clicked), workspace);
gesture = gtk_gesture_click_new();
gtk_widget_add_controller(workspace->evidence_preview_text,
GTK_EVENT_CONTROLLER(gesture));
g_signal_connect(gesture, "released",
G_CALLBACK(workspace_on_evidence_preview_clicked), workspace);
workspace->evidence_preview = evidence_preview_widget_new(
task_manager, NULL, NULL);
if (workspace->evidence_preview == NULL) {
workspace_free(workspace);
return NULL;
}
gtk_stack_set_visible_child_name(GTK_STACK(
workspace->evidence_preview_stack), "status");
gtk_widget_set_halign(workspace->evidence_preview_expand_button,
GTK_ALIGN_START);
gtk_widget_set_sensitive(workspace->evidence_preview_expand_button, FALSE);
g_signal_connect(workspace->evidence_preview_expand_button, "clicked",
G_CALLBACK(workspace_on_expand_evidence_preview), workspace);
gtk_box_append(GTK_BOX(evidence_content),
workspace->evidence_preview_stack);
gtk_box_append(GTK_BOX(evidence_content),
workspace->evidence_preview_expand_button);
evidence_preview_widget_get_widget(workspace->evidence_preview));
workspace->verify_evidence_button =
gtk_button_new_with_label(
@ -2842,9 +2686,6 @@ void workspace_set_selected_evidence(
}
workspace_clear_evidence_preview(workspace);
if (workspace->evidence_preview_stack != NULL)
gtk_stack_set_visible_child_name(GTK_STACK(
workspace->evidence_preview_stack), "status");
g_clear_pointer(
&workspace->selected_evidence_identifier,
@ -3051,99 +2892,65 @@ void workspace_set_eml_analysis_available(
void workspace_set_evidence_preview(Workspace *workspace,
const char *file_path, const char *display_name)
{
GFile *file = NULL;
GFileInfo *info = NULL;
const char *content_type = NULL;
char *mime_type = NULL;
GtkMediaStream *stream = NULL;
GError *error = NULL;
char *contents = NULL;
gsize length = 0;
char *sha256 = NULL;
char *root = NULL;
char *name = NULL;
EvidencePreviewRequest *request = NULL;
if (workspace == NULL) return;
workspace_clear_evidence_preview(workspace);
if (file_path == NULL || file_path[0] == '\0')
{
gtk_label_set_text(GTK_LABEL(workspace->evidence_preview_status),
"Aucun aperçu disponible pour cette preuve.");
gtk_stack_set_visible_child_name(GTK_STACK(
workspace->evidence_preview_stack), "status");
if (file_path == NULL || !g_file_get_contents(
file_path, &contents, &length, NULL)) return;
sha256 = g_compute_checksum_for_data(G_CHECKSUM_SHA256,
(const guchar *) contents, length);
root = g_path_get_dirname(file_path);
name = g_path_get_basename(file_path);
request = evidence_preview_request_new(root,
display_name != NULL ? display_name : name, name, sha256, NULL, 0);
evidence_preview_widget_show(workspace->evidence_preview, request,
display_name);
evidence_preview_request_free(request);
g_free(name);
g_free(root);
g_free(sha256);
g_free(contents);
}
void workspace_show_evidence_preview(Workspace *workspace,
const char *investigation_root, const EvidenceRecord *record)
{
EvidencePreviewRequest *request;
char *size;
char *metadata;
if (workspace == NULL) return;
if (investigation_root == NULL || record == NULL) {
workspace_clear_evidence_preview(workspace);
return;
}
file = g_file_new_for_path(file_path);
info = g_file_query_info(file, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
G_FILE_QUERY_INFO_NONE, NULL, &error);
if (info != NULL) content_type = g_file_info_get_content_type(info);
if (content_type != NULL) mime_type = g_content_type_get_mime_type(
content_type);
if (mime_type != NULL && g_str_has_prefix(mime_type, "image/"))
{
gtk_picture_set_filename(GTK_PICTURE(
workspace->evidence_preview_picture), file_path);
gtk_stack_set_visible_child_name(GTK_STACK(
workspace->evidence_preview_stack), "image");
workspace->evidence_preview_is_video = FALSE;
}
else if (mime_type != NULL && g_str_has_prefix(mime_type, "video/"))
{
stream = gtk_media_file_new_for_filename(file_path);
g_signal_connect(stream, "notify::error",
G_CALLBACK(workspace_on_video_error_changed), workspace);
gtk_video_set_media_stream(GTK_VIDEO(
workspace->evidence_preview_video), stream);
g_object_unref(stream);
gtk_stack_set_visible_child_name(GTK_STACK(
workspace->evidence_preview_stack), "video");
workspace->evidence_preview_is_video = TRUE;
}
else
{
char *message = NULL;
if ((mime_type != NULL && g_strcmp0(mime_type, "text/plain") == 0) ||
g_str_has_suffix(file_path, ".txt") ||
g_str_has_suffix(file_path, ".log") ||
g_str_has_suffix(file_path, ".json"))
{
char *contents = NULL;
gsize length = 0;
if (g_file_get_contents(file_path, &contents, &length, NULL))
{
GtkTextBuffer *buffer = gtk_text_view_get_buffer(
GTK_TEXT_VIEW(workspace->evidence_preview_text));
gtk_text_buffer_set_text(buffer, contents, (gint) MIN(length, 200000));
gtk_stack_set_visible_child_name(GTK_STACK(
workspace->evidence_preview_stack), "text");
workspace->evidence_preview_is_video = FALSE;
g_free(contents);
goto text_preview_ready;
}
g_free(contents);
}
message = g_strdup_printf(
"Aperçu intégré indisponible pour %s%s%s. Utilisez le bouton "
"pour ouvrir le document avec lapplication associée.",
display_name != NULL ? display_name : "ce fichier",
error != NULL ? " : " : "",
error != NULL ? error->message : "format non pris en charge");
gtk_label_set_text(GTK_LABEL(workspace->evidence_preview_status),
message);
gtk_stack_set_visible_child_name(GTK_STACK(
workspace->evidence_preview_stack), "status");
if (g_file_test(file_path, G_FILE_TEST_IS_REGULAR))
{
workspace->evidence_preview_path = g_strdup(file_path);
gtk_widget_set_sensitive(workspace->evidence_preview_expand_button,
workspace->evidence_preview_path != NULL);
}
g_free(message);
goto cleanup;
}
text_preview_ready:
workspace->evidence_preview_path = g_strdup(file_path);
gtk_widget_set_sensitive(workspace->evidence_preview_expand_button,
workspace->evidence_preview_path != NULL);
cleanup:
g_clear_error(&error);
g_free(mime_type);
g_clear_object(&info);
g_clear_object(&file);
size = g_format_size(evidence_record_get_size_bytes(record));
metadata = g_strdup_printf(
"Nom original : %s\nUUID : %s\nType métier : %s\n"
"MIME enregistré : %s\nTaille : %s\nSHA-256 : %s\n"
"Intégrité enregistrée : %s",
evidence_record_get_original_name(record),
evidence_record_get_identifier(record),
evidence_record_get_type_identifier(record),
evidence_record_get_mime_type(record) != NULL
? evidence_record_get_mime_type(record) : "Non renseigné",
size, evidence_record_get_sha256(record),
workspace_get_integrity_status_text(
evidence_record_get_integrity_status(record)));
request = evidence_preview_request_new(investigation_root,
evidence_record_get_identifier(record),
evidence_record_get_relative_path(record),
evidence_record_get_sha256(record),
evidence_record_get_mime_type(record), 0);
evidence_preview_widget_show(workspace->evidence_preview,
request, metadata);
evidence_preview_request_free(request);
g_free(metadata);
g_free(size);
}
gboolean workspace_select_graph_entity(
@ -3841,6 +3648,8 @@ void workspace_free(Workspace *workspace)
}
workspace_clear_evidence_preview(workspace);
g_clear_pointer(&workspace->evidence_preview,
evidence_preview_widget_free);
g_clear_pointer(
&workspace->osint_selection_context,

View file

@ -9,8 +9,17 @@ typedef struct
TaskManager *task_manager;
guint completion_count;
gboolean passed;
gboolean evidence_model_destroyed;
GListModel *evidence_model;
} TestContext;
static void evidence_model_destroyed(gpointer data, GObject *object)
{
TestContext *context = data;
(void) object;
context->evidence_model_destroyed = TRUE;
}
static GtkWidget *find_button(GtkWidget *widget, const char *label)
{
if (GTK_IS_BUTTON(widget) &&
@ -38,6 +47,99 @@ static GtkWidget *find_entry(GtkWidget *widget, const char *placeholder)
return NULL;
}
static GtkDropDown *find_evidence_drop_down(GtkWidget *widget)
{
if (GTK_IS_DROP_DOWN(widget)) {
GListModel *model = gtk_drop_down_get_model(GTK_DROP_DOWN(widget));
GtkStringObject *first = model != NULL &&
g_list_model_get_n_items(model) > 0
? g_list_model_get_item(model, 0) : NULL;
gboolean matches = first != NULL && g_strcmp0(
gtk_string_object_get_string(first),
"Aucune preuve associée") == 0;
g_clear_object(&first);
if (matches) return GTK_DROP_DOWN(widget);
}
for (GtkWidget *child = gtk_widget_get_first_child(widget);
child != NULL; child = gtk_widget_get_next_sibling(child)) {
GtkDropDown *match = find_evidence_drop_down(child);
if (match != NULL) return match;
}
return NULL;
}
static GtkDropDown *find_drop_down_starting_with(GtkWidget *widget,
const char *first_label)
{
if (GTK_IS_DROP_DOWN(widget)) {
GListModel *model = gtk_drop_down_get_model(GTK_DROP_DOWN(widget));
GtkStringObject *first = model != NULL &&
g_list_model_get_n_items(model) > 0
? g_list_model_get_item(model, 0) : NULL;
gboolean matches = first != NULL && g_strcmp0(
gtk_string_object_get_string(first), first_label) == 0;
g_clear_object(&first);
if (matches) return GTK_DROP_DOWN(widget);
}
for (GtkWidget *child = gtk_widget_get_first_child(widget);
child != NULL; child = gtk_widget_get_next_sibling(child)) {
GtkDropDown *match = find_drop_down_starting_with(child, first_label);
if (match != NULL) return match;
}
return NULL;
}
static GtkWidget *find_first_button(GtkWidget *widget)
{
if (GTK_IS_BUTTON(widget)) return widget;
for (GtkWidget *child = gtk_widget_get_first_child(widget);
child != NULL; child = gtk_widget_get_next_sibling(child)) {
GtkWidget *match = find_first_button(child);
if (match != NULL) return match;
}
return NULL;
}
static GtkListView *find_list_view(GtkWidget *widget, gboolean mapped_only)
{
if (GTK_IS_LIST_VIEW(widget) &&
(!mapped_only || gtk_widget_get_mapped(widget)))
return GTK_LIST_VIEW(widget);
for (GtkWidget *child = gtk_widget_get_first_child(widget);
child != NULL; child = gtk_widget_get_next_sibling(child)) {
GtkListView *match = find_list_view(child, mapped_only);
if (match != NULL) return match;
}
return NULL;
}
static GtkListView *find_popup_list_view(GtkDropDown *dropdown)
{
GtkWidget *root = GTK_WIDGET(gtk_widget_get_root(GTK_WIDGET(dropdown)));
GtkListView *list_view;
GtkWidget *button = find_first_button(GTK_WIDGET(dropdown));
g_assert_nonnull(button);
if (GTK_IS_TOGGLE_BUTTON(button))
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), TRUE);
else
gtk_widget_activate(button);
while (g_main_context_iteration(NULL, FALSE)) {}
list_view = find_list_view(GTK_WIDGET(dropdown), TRUE);
if (list_view == NULL && root != NULL)
list_view = find_list_view(root, TRUE);
return list_view;
}
static void activate_popup_item(GtkListView *list_view, guint position)
{
GtkSelectionModel *selection = gtk_list_view_get_model(list_view);
g_assert_nonnull(selection);
g_assert_true(gtk_selection_model_select_item(
selection, position, TRUE));
g_signal_emit_by_name(list_view, "activate", position);
while (g_main_context_iteration(NULL, FALSE)) {}
}
static GtkWidget *find_label_containing(GtkWidget *widget, const char *text)
{
if (GTK_IS_LABEL(widget) &&
@ -63,6 +165,7 @@ 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(context->evidence_model_destroyed);
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);
@ -78,6 +181,7 @@ static gboolean click_cancel(gpointer data)
GtkWindow *dialog = windows != NULL && windows->data != context->main_window
? windows->data : windows != NULL ? windows->next->data : NULL;
GtkWidget *button, *designation, *search;
GtkDropDown *evidence, *retained_type, *type_filter;
g_assert_nonnull(dialog);
designation = find_entry(GTK_WIDGET(dialog),
"Personne présumée liée aux comptes");
@ -90,13 +194,70 @@ static gboolean click_cancel(gpointer data)
search = find_entry(GTK_WIDGET(dialog),
"Rechercher par nom, description ou type");
g_assert_nonnull(search);
gtk_editable_set_text(GTK_EDITABLE(search), "SPECIMEN");
evidence = find_evidence_drop_down(GTK_WIDGET(dialog));
g_assert_nonnull(evidence);
context->evidence_model = gtk_drop_down_get_model(evidence);
g_assert_nonnull(context->evidence_model);
g_object_weak_ref(G_OBJECT(context->evidence_model),
evidence_model_destroyed, context);
GtkListView *popup_list = find_popup_list_view(evidence);
g_assert_nonnull(popup_list);
activate_popup_item(popup_list, 1);
popup_list = find_popup_list_view(evidence);
g_assert_nonnull(popup_list);
activate_popup_item(popup_list, 2);
popup_list = find_popup_list_view(evidence);
g_assert_nonnull(popup_list);
activate_popup_item(popup_list, 0);
popup_list = find_popup_list_view(evidence);
g_assert_nonnull(popup_list);
activate_popup_item(popup_list, 1);
gtk_editable_set_text(GTK_EDITABLE(search), "recherche");
g_assert_true(gtk_drop_down_get_model(evidence) ==
context->evidence_model);
g_assert_false(context->evidence_model_destroyed);
type_filter = find_drop_down_starting_with(
GTK_WIDGET(dialog), "Tous les types");
g_assert_nonnull(type_filter);
popup_list = find_popup_list_view(type_filter);
g_assert_nonnull(popup_list);
activate_popup_item(popup_list, 2);
popup_list = find_popup_list_view(evidence);
g_assert_nonnull(popup_list);
activate_popup_item(popup_list, 1);
g_assert_cmpuint(gtk_drop_down_get_selected(evidence), ==, 1);
button = find_button(GTK_WIDGET(dialog), "Ajouter à la sélection");
g_assert_nonnull(button);
g_signal_emit_by_name(button, "clicked");
retained_type = find_drop_down_starting_with(
GTK_WIDGET(dialog), "Capture décran");
g_assert_nonnull(retained_type);
gtk_drop_down_set_selected(retained_type, 4);
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
"Type : email (email)"));
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog), "— email"));
popup_list = find_popup_list_view(type_filter);
g_assert_nonnull(popup_list);
activate_popup_item(popup_list, 0);
gtk_editable_set_text(GTK_EDITABLE(search), "AUTRE");
g_assert_true(gtk_drop_down_get_model(evidence) ==
context->evidence_model);
popup_list = find_popup_list_view(evidence);
g_assert_nonnull(popup_list);
activate_popup_item(popup_list, 1);
button = find_button(GTK_WIDGET(dialog), "Ajouter à la sélection");
g_assert_nonnull(button);
g_signal_emit_by_name(button, "clicked");
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),
"Type métier : email"));
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
"AUTRE-SPECIMEN.jpg"));
g_assert_nonnull(find_label_containing(GTK_WIDGET(dialog),
"Aucune écriture na encore été effectuée"));
button = find_button(GTK_WIDGET(dialog), "Annuler");
@ -124,6 +285,17 @@ static void activate(GtkApplication *application, gpointer data)
evidence_record_set_display_metadata(
record, "Capture décran", NULL);
g_ptr_array_add(records, record);
record = evidence_record_new(
"10000000-0000-4000-8000-000000000002",
"AUTRE-SPECIMEN.jpg", "autre.jpg",
"01_Preuves_Originales/autre.jpg", "photo", 24,
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"2026-07-28T10:00:00Z", NULL, NULL,
"Deuxième preuve synthétique", EVIDENCE_INTEGRITY_STATUS_UNKNOWN,
&error);
g_assert_no_error(error);
evidence_record_set_display_metadata(record, "Photo", "image/jpeg");
g_ptr_array_add(records, record);
context->main_window = GTK_WINDOW(
gtk_application_window_new(application));
gtk_window_present(context->main_window);

View file

@ -1,8 +1,10 @@
#include "core/evidence_preview.h"
#include "core/file_hash.h"
#include <cairo.h>
#include <cairo-pdf.h>
#include <stdio.h>
#include <jpeglib.h>
#include <libheif/heif.h>
#include "core/evidence_preview_task.h"
#include <glib/gstdio.h>
@ -31,6 +33,7 @@ static void test_preview_png_and_guards(void)
result = evidence_preview_load(request, NULL, &error);
g_assert_no_error(error);
g_assert_nonnull(result);
g_assert_cmpstr(result->effective_mime_type, ==, "image/png");
g_assert_true(evidence_preview_result_matches(result,
"10000000-0000-4000-8000-000000000109", 7));
g_assert_false(evidence_preview_result_matches(result,
@ -94,7 +97,7 @@ static void test_preview_jpeg_async(void)
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);
sha, NULL, 9);
BackgroundTask *task = evidence_preview_task_start(manager, request,
preview_done, &state, NULL, &error);
g_assert_nonnull(task);
@ -104,6 +107,7 @@ static void test_preview_jpeg_async(void)
g_assert_nonnull(result);
g_assert_cmpint(result->width, ==, 32);
g_assert_cmpint(result->height, ==, 20);
g_assert_cmpstr(result->effective_mime_type, ==, "image/jpeg");
background_task_unref(task);
evidence_preview_request_free(request);
task_manager_free(manager); g_main_loop_unref(state.loop);
@ -111,6 +115,257 @@ static void test_preview_jpeg_async(void)
g_free(sha); g_free(path); g_free(root);
}
static void test_preview_rejects_false_jpeg(void)
{
GError *error = NULL;
char *root = g_dir_make_tmp("labfy-preview-invalid-XXXXXX", &error);
char *path = g_build_filename(root, "SPECIMEN.jpg", NULL);
char *sha = NULL;
guint64 size = 0;
EvidencePreviewRequest *request;
g_assert_true(g_file_set_contents(path, "SPECIMEN non image", -1,
&error));
g_assert_true(file_hash_compute_sha256(path, NULL, &sha, &size, &error));
request = evidence_preview_request_new(root,
"10000000-0000-4000-8000-000000000111", "SPECIMEN.jpg",
sha, "image/jpeg", 10);
EvidencePreviewResult *result =
evidence_preview_load(request, NULL, &error);
g_assert_no_error(error);
g_assert_nonnull(result);
g_assert_cmpint(result->kind, ==, EVIDENCE_PREVIEW_KIND_UNSUPPORTED);
evidence_preview_result_free(result);
evidence_preview_request_free(request);
g_assert_cmpint(g_remove(path), ==, 0);
g_assert_cmpint(g_rmdir(root), ==, 0);
g_free(sha);
g_free(path);
g_free(root);
}
static EvidencePreviewResult *load_fixture(const char *root,
const char *name, const guint8 *data, gsize length, const char *mime)
{
GError *error = NULL;
char *path = g_build_filename(root, name, NULL);
char *sha = NULL; guint64 size = 0;
g_assert_true(g_file_set_contents(path, (const char *) data,
(gssize) length, &error));
g_assert_true(file_hash_compute_sha256(path, NULL, &sha, &size, &error));
EvidencePreviewRequest *request = evidence_preview_request_new(root,
"10000000-0000-4000-8000-000000000112", name, sha, mime, 11);
EvidencePreviewResult *result = evidence_preview_load(request, NULL, &error);
g_assert_no_error(error); g_assert_nonnull(result);
evidence_preview_request_free(request); g_free(sha); g_free(path);
return result;
}
static void test_preview_dispatch_text_video_email(void)
{
GError *error = NULL;
char *root = g_dir_make_tmp("labfy-preview-dispatch-XXXXXX", &error);
static const guint8 text[] = "SPECIMEN,évidence\n1,contrôlée\n";
static const guint8 mp4[] = {
0,0,0,24,'f','t','y','p','i','s','o','m',0,0,0,0,
'i','s','o','m','m','p','4','2'};
static const guint8 mov[] = {
0,0,0,20,'f','t','y','p','q','t',' ',' ',0,0,0,0,'q','t',' ',' '};
static const guint8 eml[] =
"From: Alice SPECIMEN <alice@example.com>\r\n"
"To: Bob SPECIMEN <bob@example.com>\r\n"
"Date: Tue, 28 Jul 2026 10:00:00 +0200\r\n"
"Subject: SPECIMEN\r\nMessage-ID: <specimen@example.com>\r\n"
"Content-Type: text/plain; charset=UTF-8\r\n\r\n"
"Corps SPECIMEN sans ressource distante.\r\n";
EvidencePreviewResult *result = load_fixture(root, "SPECIMEN.csv",
text, sizeof(text) - 1, NULL);
g_assert_cmpint(result->kind, ==, EVIDENCE_PREVIEW_KIND_TEXT);
g_assert_nonnull(strstr(result->text, "évidence"));
evidence_preview_result_free(result);
result = load_fixture(root, "SPECIMEN.bin", mp4, sizeof(mp4), "text/plain");
g_assert_cmpint(result->kind, ==, EVIDENCE_PREVIEW_KIND_VIDEO);
g_assert_cmpstr(result->effective_mime_type, ==, "video/mp4");
evidence_preview_result_free(result);
result = load_fixture(root, "SPECIMEN.data", mov, sizeof(mov), NULL);
g_assert_cmpint(result->kind, ==, EVIDENCE_PREVIEW_KIND_VIDEO);
g_assert_cmpstr(result->effective_mime_type, ==, "video/quicktime");
evidence_preview_result_free(result);
result = load_fixture(root, "SPECIMEN.eml", eml, sizeof(eml) - 1, NULL);
g_assert_cmpint(result->kind, ==, EVIDENCE_PREVIEW_KIND_EMAIL);
g_assert_nonnull(strstr(result->text, "Alice SPECIMEN"));
evidence_preview_result_free(result);
g_remove(g_build_filename(root, "SPECIMEN.csv", NULL));
g_remove(g_build_filename(root, "SPECIMEN.bin", NULL));
g_remove(g_build_filename(root, "SPECIMEN.data", NULL));
g_remove(g_build_filename(root, "SPECIMEN.eml", NULL));
g_assert_cmpint(g_rmdir(root), ==, 0); g_free(root);
}
static void test_preview_pdf_first_page(void)
{
GError *error = NULL;
char *root = g_dir_make_tmp("labfy-preview-pdf-XXXXXX", &error);
char *path = g_build_filename(root, "SPECIMEN.pdf", NULL);
cairo_surface_t *surface = cairo_pdf_surface_create(path, 320, 200);
cairo_t *cr = cairo_create(surface);
cairo_set_source_rgb(cr, 0, 0, 0); cairo_move_to(cr, 20, 40);
cairo_show_text(cr, "SPECIMEN"); cairo_show_page(cr);
cairo_destroy(cr); cairo_surface_destroy(surface);
char *contents = NULL; gsize length = 0;
g_assert_true(g_file_get_contents(path, &contents, &length, &error));
EvidencePreviewResult *result = load_fixture(root, "SPECIMEN.pdf",
(const guint8 *) contents, length, "application/octet-stream");
g_assert_cmpint(result->kind, ==, EVIDENCE_PREVIEW_KIND_PDF);
g_assert_nonnull(result->png_bytes); g_assert_cmpuint(result->item_count, ==, 1);
evidence_preview_result_free(result);
g_free(contents); g_assert_cmpint(g_remove(path), ==, 0);
g_assert_cmpint(g_rmdir(root), ==, 0); g_free(path); g_free(root);
}
static gboolean write_heic_specimen(const char *path, gint width, gint height)
{
struct heif_context *context = heif_context_alloc();
struct heif_encoder *encoder = NULL;
struct heif_image *image = NULL;
struct heif_image_handle *handle = NULL;
struct heif_error status = heif_context_get_encoder_for_format(
context, heif_compression_HEVC, &encoder);
if (status.code != heif_error_Ok) {
heif_context_free(context); return FALSE;
}
status = heif_image_create(width, height, heif_colorspace_RGB,
heif_chroma_interleaved_RGB, &image);
if (status.code == heif_error_Ok)
status = heif_image_add_plane(image, heif_channel_interleaved,
width, height, 8);
if (status.code == heif_error_Ok) {
gint stride = 0;
guint8 *pixels = heif_image_get_plane(image,
heif_channel_interleaved, &stride);
for (gint y = 0; y < height; y++)
for (gint x = 0; x < width; x++) {
pixels[y * stride + x * 3] = (guint8) (x % 255);
pixels[y * stride + x * 3 + 1] = (guint8) (y % 255);
pixels[y * stride + x * 3 + 2] = 96;
}
status = heif_context_encode_image(context, image, encoder,
NULL, &handle);
}
if (status.code == heif_error_Ok)
status = heif_context_write_to_file(context, path);
if (handle != NULL) heif_image_handle_release(handle);
if (image != NULL) heif_image_release(image);
heif_encoder_release(encoder); heif_context_free(context);
return status.code == heif_error_Ok;
}
static void test_preview_real_heic(void)
{
GError *error = NULL;
char *root = g_dir_make_tmp("labfy-preview-heic-XXXXXX", &error);
char *path = g_build_filename(root, "SPECIMEN", NULL);
if (!write_heic_specimen(path, 48, 32)) {
g_test_skip("Aucun encodeur HEVC libheif disponible.");
g_free(path); g_rmdir(root); g_free(root); return;
}
char *contents = NULL; gsize length = 0;
g_assert_true(g_file_get_contents(path, &contents, &length, &error));
EvidencePreviewResult *result = load_fixture(root, "SPECIMEN",
(const guint8 *) contents, length, NULL);
g_assert_cmpint(result->kind, ==, EVIDENCE_PREVIEW_KIND_IMAGE);
g_assert_cmpstr(result->effective_mime_type, ==, "image/heic");
g_assert_cmpint(result->width, ==, 48);
g_assert_cmpint(result->height, ==, 32);
g_assert_cmpuint(result->item_count, ==, 1);
evidence_preview_result_free(result);
for (gsize offset = 0; offset + 4 <= length; offset++)
if (memcmp(contents + offset, "heic", 4) == 0)
memcpy(contents + offset, "mif1", 4);
result = load_fixture(root, "SPECIMEN.heif",
(const guint8 *) contents, length, NULL);
g_assert_cmpint(result->kind, ==, EVIDENCE_PREVIEW_KIND_IMAGE);
g_assert_cmpstr(result->effective_mime_type, ==, "image/heif");
evidence_preview_result_free(result);
char *heif_path = g_build_filename(root, "SPECIMEN.heif", NULL);
g_assert_cmpint(g_remove(heif_path), ==, 0); g_free(heif_path);
g_free(contents); g_assert_cmpint(g_remove(path), ==, 0);
g_assert_cmpint(g_rmdir(root), ==, 0); g_free(path); g_free(root);
}
static void test_preview_heic_cancel_and_invalid(void)
{
GError *error = NULL;
char *root = g_dir_make_tmp("labfy-preview-heic-invalid-XXXXXX", &error);
static const guint8 invalid[] = {
0,0,0,20,'f','t','y','p','h','e','i','c',0,0,0,0,'m','i','f','1'};
char *path = g_build_filename(root, "SPECIMEN.heic", NULL);
g_assert_true(g_file_set_contents(path, (const char *) invalid,
sizeof(invalid), &error));
char *sha = NULL; guint64 size = 0;
g_assert_true(file_hash_compute_sha256(path, NULL, &sha, &size, &error));
EvidencePreviewRequest *request = evidence_preview_request_new(root,
"10000000-0000-4000-8000-000000000113", "SPECIMEN.heic",
sha, NULL, 12);
g_assert_null(evidence_preview_load(request, NULL, &error));
g_assert_nonnull(error); g_clear_error(&error);
GCancellable *cancellable = g_cancellable_new();
g_cancellable_cancel(cancellable);
g_assert_null(evidence_preview_load(request, cancellable, &error));
g_assert_nonnull(error);
g_clear_error(&error); g_object_unref(cancellable);
evidence_preview_request_free(request); g_free(sha);
g_assert_cmpint(g_remove(path), ==, 0);
g_assert_cmpint(g_rmdir(root), ==, 0); g_free(path); g_free(root);
}
static void test_preview_dimension_limits_without_allocation(void)
{
g_assert_true(evidence_preview_dimensions_are_safe(8000, 5000));
g_assert_false(evidence_preview_dimensions_are_safe(8001, 5000));
g_assert_false(evidence_preview_dimensions_are_safe(12001, 1));
g_assert_false(evidence_preview_dimensions_are_safe(1, 12001));
g_assert_false(evidence_preview_dimensions_are_safe(0, 100));
}
static void test_preview_eml_multipart_inventory(void)
{
GError *error = NULL;
char *root = g_dir_make_tmp("labfy-preview-eml-XXXXXX", &error);
static const guint8 eml[] =
"From: Alice SPECIMEN <alice@example.com>\r\n"
"To: Bob <bob@example.com>, Eve <eve@example.com>\r\n"
"Subject: =?UTF-8?Q?Sujet_=C3=A9tude_SPECIMEN?=\r\n"
"Date: Tue, 28 Jul 2026 10:00:00 +0200\r\n"
"Message-ID: <nested@example.com>\r\nMIME-Version: 1.0\r\n"
"Content-Type: multipart/mixed; boundary=outer\r\n\r\n"
"--outer\r\nContent-Type: multipart/alternative; boundary=inner\r\n\r\n"
"--inner\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n"
"<style>secret{}</style><p>HTML distant<img src=\"https://example.invalid/pixel\"></p>\r\n"
"--inner\r\nContent-Type: text/plain; charset=UTF-8\r\n"
"Content-Transfer-Encoding: quoted-printable\r\n\r\n"
"Corps=20pr=C3=A9f=C3=A9r=C3=A9=20SPECIMEN\r\n--inner--\r\n"
"--outer\r\nContent-Type: text/plain\r\n"
"Content-Disposition: attachment; filename*0*=UTF-8''rapport%20;"
" filename*1*=synth%C3%A9tique.txt\r\n"
"Content-Transfer-Encoding: base64\r\n\r\nU1BFQ0lNRU4=\r\n"
"--outer\r\nContent-Type: image/png; name=\"inline.png\"\r\n"
"Content-Disposition: inline\r\nContent-ID: <specimen-inline@example.com>\r\n"
"Content-Transfer-Encoding: base64\r\n\r\nUE5H\r\n--outer--\r\n";
EvidencePreviewResult *result = load_fixture(root, "SPECIMEN.eml",
eml, sizeof(eml) - 1, "message/rfc822");
g_assert_cmpint(result->kind, ==, EVIDENCE_PREVIEW_KIND_EMAIL);
g_assert_cmpuint(result->item_count, ==, 2);
g_assert_nonnull(strstr(result->text, "Corps préféré SPECIMEN"));
g_assert_nonnull(strstr(result->text, "rapport synthétique.txt"));
g_assert_nonnull(strstr(result->text, "inline.png"));
g_assert_nonnull(strstr(result->text, "Content-ID: specimen-inline@example.com"));
g_assert_null(strstr(result->text, "--outer"));
evidence_preview_result_free(result);
char *path = g_build_filename(root, "SPECIMEN.eml", NULL);
g_assert_cmpint(g_remove(path), ==, 0);
g_assert_cmpint(g_rmdir(root), ==, 0); g_free(path); g_free(root);
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
@ -118,5 +373,19 @@ int main(int argc, char **argv)
test_preview_png_and_guards);
g_test_add_func("/evidence-preview/jpeg-async",
test_preview_jpeg_async);
g_test_add_func("/evidence-preview/rejects-false-jpeg",
test_preview_rejects_false_jpeg);
g_test_add_func("/evidence-preview/dispatch-text-video-email",
test_preview_dispatch_text_video_email);
g_test_add_func("/evidence-preview/pdf-first-page",
test_preview_pdf_first_page);
g_test_add_func("/evidence-preview/real-heic",
test_preview_real_heic);
g_test_add_func("/evidence-preview/heic-cancel-invalid",
test_preview_heic_cancel_and_invalid);
g_test_add_func("/evidence-preview/eml-multipart-inventory",
test_preview_eml_multipart_inventory);
g_test_add_func("/evidence-preview/dimension-limits",
test_preview_dimension_limits_without_allocation);
return g_test_run();
}

View file

@ -0,0 +1,84 @@
#include "widgets/evidence_preview_widget.h"
#include <glib/gstdio.h>
static void drain_until(EvidencePreviewWidget *widget, const char *state)
{
gint64 deadline = g_get_monotonic_time() + 5 * G_TIME_SPAN_SECOND;
while (g_strcmp0(evidence_preview_widget_get_state(widget), state) != 0 &&
g_get_monotonic_time() < deadline)
g_main_context_iteration(NULL, TRUE);
}
static void test_widget_lifecycle_and_eml(void)
{
static const char eml[] =
"From: alice@example.test\r\n"
"To: bob@example.test\r\n"
"Cc: copie@example.test\r\n"
"Date: Tue, 28 Jul 2026 10:00:00 +0200\r\n"
"Subject: SPECIMEN partagé\r\n"
"Message-ID: <specimen@example.test>\r\n"
"Content-Type: multipart/mixed; boundary=x\r\n\r\n"
"--x\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n"
"Corps synthétique.\r\n"
"--x\r\nContent-Type: text/plain; name=piece.txt\r\n"
"Content-Disposition: attachment; filename=piece.txt\r\n\r\n"
"contenu\r\n--x--\r\n";
GError *error = NULL;
char *directory = g_dir_make_tmp("labfy-preview-widget-XXXXXX", &error);
char *path;
char *sha;
EvidencePreviewRequest *request;
TaskManager *manager;
EvidencePreviewWidget *widget;
char *text;
if (directory == NULL) {
g_test_skip(error->message);
g_clear_error(&error);
return;
}
path = g_build_filename(directory, "specimen.eml", NULL);
g_assert_true(g_file_set_contents(path, eml, -1, &error));
g_assert_no_error(error);
sha = g_compute_checksum_for_string(G_CHECKSUM_SHA256, eml, -1);
manager = task_manager_new();
widget = evidence_preview_widget_new(manager, NULL, NULL);
g_assert_nonnull(widget);
g_assert_cmpstr(evidence_preview_widget_get_state(widget), ==, "empty");
request = evidence_preview_request_new(directory,
"11111111-1111-4111-8111-111111111111",
"specimen.eml", sha, NULL, 1);
evidence_preview_widget_show(widget, request, "SPECIMEN");
g_assert_cmpstr(evidence_preview_widget_get_state(widget), ==, "loading");
drain_until(widget, "email");
g_assert_cmpstr(evidence_preview_widget_get_state(widget), ==, "email");
text = evidence_preview_widget_dup_text(widget);
g_assert_nonnull(g_strstr_len(text, -1, "alice@example.test"));
g_assert_nonnull(g_strstr_len(text, -1, "Corps synthétique."));
g_assert_nonnull(g_strstr_len(text, -1, "piece.txt"));
g_free(text);
evidence_preview_widget_clear(widget);
g_assert_cmpstr(evidence_preview_widget_get_state(widget), ==, "empty");
evidence_preview_widget_cancel(widget);
evidence_preview_widget_cancel(widget);
evidence_preview_widget_free(widget);
task_manager_free(manager);
evidence_preview_request_free(request);
g_remove(path);
g_rmdir(directory);
g_free(sha);
g_free(path);
g_free(directory);
}
int main(int argc, char **argv)
{
if (!gtk_init_check()) {
g_print("SKIP: aucun affichage GTK disponible.\n");
return 0;
}
g_test_init(&argc, &argv, NULL);
g_test_add_func("/evidence-preview-widget/eml-lifecycle",
test_widget_lifecycle_and_eml);
return g_test_run();
}

View file

@ -0,0 +1,70 @@
#include "core/evidence_staging.h"
#include <glib.h>
#include <glib/gstdio.h>
static void test_prepare_cleanup(void)
{
char *directory = g_dir_make_tmp("labfy-staging-test-XXXXXX", NULL);
char *source = g_build_filename(directory, "SPECIMEN été.png", NULL);
const char content[] = "\x89PNG\r\n\x1a\nSPECIMEN";
EvidenceStaging *staging;
EvidenceStagingResult *result;
GError *error = NULL;
g_assert_true(g_file_set_contents(source, content,
sizeof content - 1, &error));
staging = evidence_staging_new(&error);
result = evidence_staging_prepare(staging, source, NULL, &error);
g_assert_no_error(error);
g_assert_nonnull(result);
g_assert_cmpstr(result->mime_type, ==, "image/png");
g_assert_cmpstr(result->suggested_type, ==, "screenshot");
g_assert_true(g_file_test(result->staging_path, G_FILE_TEST_EXISTS));
{
char *after = NULL;
gsize length = 0;
g_assert_true(g_file_get_contents(source, &after, &length, NULL));
g_assert_cmpmem(after, length, content, sizeof content - 1);
g_free(after);
}
g_assert_true(evidence_staging_remove(
staging, result->staging_path, &error));
g_assert_false(g_file_test(result->staging_path, G_FILE_TEST_EXISTS));
evidence_staging_result_free(result);
evidence_staging_free(staging);
g_unlink(source); g_rmdir(directory);
g_free(source); g_free(directory);
}
static void test_invalid_sources(void)
{
char *directory = g_dir_make_tmp("labfy-staging-invalid-XXXXXX", NULL);
char *link_path = g_build_filename(directory, "SPECIMEN-link", NULL);
EvidenceStaging *staging = evidence_staging_new(NULL);
GError *error = NULL;
g_assert_null(evidence_staging_prepare(
staging, directory, NULL, &error));
g_clear_error(&error);
{
GFile *link_file = g_file_new_for_path(link_path);
g_assert_true(g_file_make_symbolic_link(
link_file, directory, NULL, NULL));
g_object_unref(link_file);
}
g_assert_null(evidence_staging_prepare(
staging, link_path, NULL, &error));
g_clear_error(&error);
g_assert_null(evidence_staging_prepare(
staging, "/tmp/SPECIMEN-absent", NULL, &error));
g_clear_error(&error);
evidence_staging_free(staging);
g_unlink(link_path); g_rmdir(directory);
g_free(link_path); g_free(directory);
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
g_test_add_func("/evidence-staging/prepare-cleanup",
test_prepare_cleanup);
g_test_add_func("/evidence-staging/invalid-sources",
test_invalid_sources);
return g_test_run();
}

View file

@ -0,0 +1,41 @@
#include "core/evidence_video_preview_controller.h"
#include <glib.h>
typedef struct { guint pause, seek, detach, release; } Effects;
typedef struct { Effects *effects; } FakeMedia;
static void pause_media(gpointer data)
{ ((FakeMedia *) data)->effects->pause++; }
static void seek_media(gpointer data)
{ ((FakeMedia *) data)->effects->seek++; }
static void detach_media(gpointer data)
{ ((Effects *) data)->detach++; }
static void release_media(gpointer data)
{ ((FakeMedia *) data)->effects->release++; g_free(data); }
static FakeMedia *media_new(Effects *effects)
{ FakeMedia *media = g_new0(FakeMedia, 1); media->effects = effects; return media; }
static void test_replace_close(void)
{
Effects effects = {0};
EvidenceVideoPreviewActions actions = {
pause_media, seek_media, detach_media, release_media};
EvidenceVideoPreviewController *controller =
evidence_video_preview_controller_new(&effects, &actions);
g_assert_false(evidence_video_preview_controller_has_media(controller));
evidence_video_preview_controller_replace(controller, media_new(&effects));
g_assert_true(evidence_video_preview_controller_has_media(controller));
evidence_video_preview_controller_replace(controller, media_new(&effects));
g_assert_cmpuint(effects.pause, ==, 1);
g_assert_cmpuint(effects.seek, ==, 1);
g_assert_cmpuint(effects.detach, ==, 1);
g_assert_cmpuint(effects.release, ==, 1);
evidence_video_preview_controller_free(controller);
g_assert_cmpuint(effects.pause, ==, 2);
g_assert_cmpuint(effects.seek, ==, 2);
g_assert_cmpuint(effects.detach, ==, 2);
g_assert_cmpuint(effects.release, ==, 2);
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
g_test_add_func("/evidence-video/replace-close", test_replace_close);
return g_test_run();
}

View file

@ -50,11 +50,39 @@ static void test_optional_summary(void)
"Aucune preuve principale associée"));
g_free(summary);
}
static void test_multiple_summary(void)
{
EvidenceRecord *evidence = record();
PersonEvidenceSelection *selection = person_evidence_selection_new();
GPtrArray *roles = g_ptr_array_new();
char *summary;
g_ptr_array_add(roles, "Témoin");
g_assert_true(person_evidence_selection_add_existing(
selection, evidence, NULL));
g_assert_true(person_evidence_selection_add_staged(selection,
"/tmp/SPECIMEN-source.pdf", "/tmp/SPECIMEN-stage.pdf",
"SPECIMEN-document.pdf", "application/pdf", "document", 100,
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
NULL, "2026-07-28T10:00:00Z", NULL));
summary = person_confirmation_summary_build_multiple(
"Personne SPECIMEN", NULL, NULL, "Présumé", 40, NULL,
roles, selection);
g_assert_nonnull(strstr(summary, "SPECIMEN-confirmation.png"));
g_assert_nonnull(strstr(summary, "SPECIMEN-document.pdf"));
g_assert_nonnull(strstr(summary, "1 nouvelle(s) preuve(s)"));
g_assert_nonnull(strstr(summary, "1 preuve(s) existante(s)"));
g_assert_nonnull(strstr(summary, "Aucun OCR"));
g_free(summary);
g_ptr_array_unref(roles);
person_evidence_selection_free(selection);
evidence_record_free(evidence);
}
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);
g_test_add_func("/person-confirmation/multiple", test_multiple_summary);
return g_test_run();
}

View file

@ -0,0 +1,114 @@
#include "core/evidence_staging.h"
#include "core/person_creation_coordinator.h"
#include "database/database.h"
#include "database/statement.h"
#include <glib.h>
#include <glib/gstdio.h>
static guint64 count(Database *database, const char *table)
{
char *sql = g_strdup_printf("SELECT COUNT(*) FROM %s;", table);
DatabaseStatement *statement = database_statement_prepare(database, sql);
int64_t value = 0;
g_assert_nonnull(statement);
g_assert_cmpint(database_statement_step(statement), ==,
DATABASE_STATEMENT_STEP_ROW);
g_assert_true(database_statement_column_int64(statement, 0, &value));
database_statement_finalize(statement);
g_free(sql);
return (guint64) value;
}
static void remove_tree(const char *root)
{
GDir *directory = g_dir_open(root, 0, NULL);
const char *name;
if (directory == NULL) return;
while ((name = g_dir_read_name(directory)) != NULL) {
char *path = g_build_filename(root, name, NULL);
if (g_file_test(path, G_FILE_TEST_IS_DIR)) remove_tree(path);
else g_unlink(path);
g_free(path);
}
g_dir_close(directory);
g_rmdir(root);
}
static void test_success_and_rollback(void)
{
char *root = g_dir_make_tmp("labfy-coordinator-XXXXXX", NULL);
char *database_directory =
g_build_filename(root, "00_BaseDeDonnees", NULL);
char *database_path =
g_build_filename(database_directory, "Enquete.sqlite", NULL);
char *source_one = g_build_filename(root, "SPECIMEN-one.txt", NULL);
char *source_two = g_build_filename(root, "SPECIMEN-two.txt", NULL);
EvidenceStaging *staging = NULL;
EvidenceStagingResult *prepared_one = NULL, *prepared_two = NULL;
PersonEvidenceSelection *selection = NULL;
PersonCreationCoordinatorResult *result = NULL;
Database *database;
PersonEntityInput person = {
.designation = "Personne SPECIMEN",
.identification_status = "suspected",
.confidence = 20
};
GError *error = NULL;
g_assert_cmpint(g_mkdir_with_parents(database_directory, 0700), ==, 0);
g_assert_true(database_initialize(
database_path, "Enquête SPECIMEN", root));
g_assert_true(g_file_set_contents(
source_one, "SPECIMEN-ONE", -1, NULL));
g_assert_true(g_file_set_contents(
source_two, "SPECIMEN-TWO", -1, NULL));
staging = evidence_staging_new(&error);
prepared_one = evidence_staging_prepare(
staging, source_one, NULL, &error);
prepared_two = evidence_staging_prepare(
staging, source_two, NULL, &error);
g_assert_no_error(error);
selection = person_evidence_selection_new();
g_assert_true(person_evidence_selection_add_staged(selection,
prepared_one->source_path, prepared_one->staging_path,
prepared_one->original_name, prepared_one->mime_type, "text",
prepared_one->size_bytes, prepared_one->sha256, NULL,
prepared_one->prepared_at, &error));
database = database_open(database_path);
result = person_creation_coordinator_execute(database, root, &person,
selection, NULL, &error);
g_assert_no_error(error);
g_assert_nonnull(result);
g_assert_cmpuint(result->evidence_identifiers->len, ==, 1);
g_assert_cmpuint(count(database, "entites"), ==, 1);
g_assert_cmpuint(count(database, "preuves"), ==, 1);
g_assert_cmpuint(count(database, "preuve_entites"), ==, 1);
g_assert_cmpuint(count(database, "preuve_entite_sources"), ==, 1);
person_creation_coordinator_result_free(result);
person_evidence_selection_free(selection);
selection = person_evidence_selection_new();
g_assert_true(person_evidence_selection_add_staged(selection,
prepared_two->source_path, prepared_two->staging_path,
prepared_two->original_name, prepared_two->mime_type, "missing_type",
prepared_two->size_bytes, prepared_two->sha256, NULL,
prepared_two->prepared_at, &error));
result = person_creation_coordinator_execute(database, root, &person,
selection, NULL, &error);
g_assert_null(result);
g_assert_nonnull(error);
g_clear_error(&error);
g_assert_cmpuint(count(database, "entites"), ==, 1);
g_assert_cmpuint(count(database, "preuves"), ==, 1);
database_close(database);
person_evidence_selection_free(selection);
evidence_staging_result_free(prepared_one);
evidence_staging_result_free(prepared_two);
evidence_staging_free(staging);
g_free(database_directory); g_free(database_path);
g_free(source_one); g_free(source_two);
remove_tree(root); g_free(root);
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
g_test_add_func("/person-coordinator/success-rollback",
test_success_and_rollback);
return g_test_run();
}

View file

@ -0,0 +1,117 @@
#include "models/person_evidence_selection.h"
#include <glib.h>
#include <string.h>
static EvidenceRecord *record(const char *identifier, const char *name,
const char *sha)
{
EvidenceRecord *record = evidence_record_new(identifier, name, name,
"01_Preuves_Originales/specimen.png", "screenshot", 12, sha,
"2026-07-28T10:00:00Z", NULL, NULL, "SPECIMEN",
EVIDENCE_INTEGRITY_STATUS_VALID, NULL);
evidence_record_set_display_metadata(record, "Capture", "image/png");
return record;
}
static void test_collection(void)
{
PersonEvidenceSelection *selection = person_evidence_selection_new();
EvidenceRecord *first = record(
"10000000-0000-4000-8000-000000000001", "SPECIMEN-1.png",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
EvidenceRecord *second = record(
"10000000-0000-4000-8000-000000000002", "SPECIMEN-2.png",
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
GError *error = NULL;
g_assert_cmpuint(person_evidence_selection_get_count(selection), ==, 0);
g_assert_true(person_evidence_selection_add_existing(
selection, first, &error));
g_assert_true(person_evidence_selection_add_existing(
selection, second, &error));
g_assert_true(person_evidence_selection_add_staged(selection,
"/tmp/SPECIMEN-source.jpg", "/tmp/SPECIMEN-stage.jpg",
"SPECIMEN.jpg", "image/jpeg", "photo", 24,
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
"description", "2026-07-28T10:00:00Z", &error));
g_assert_cmpuint(person_evidence_selection_get_count(selection), ==, 3);
g_assert_cmpstr(person_evidence_selection_item_get_original_name(
person_evidence_selection_get(selection, 0)), ==, "SPECIMEN-1.png");
g_assert_true(person_evidence_selection_set_active(selection,
person_evidence_selection_item_get_identifier(
person_evidence_selection_get(selection, 2))));
g_assert_cmpstr(person_evidence_selection_item_get_original_name(
person_evidence_selection_get_active(selection)), ==, "SPECIMEN.jpg");
g_assert_true(person_evidence_selection_set_type(selection,
person_evidence_selection_item_get_identifier(
person_evidence_selection_get(selection, 2)), "document"));
g_assert_true(person_evidence_selection_remove(selection,
person_evidence_selection_item_get_identifier(
person_evidence_selection_get(selection, 1))));
g_assert_cmpuint(person_evidence_selection_get_count(selection), ==, 2);
g_assert_true(person_evidence_selection_is_confirmable(selection));
evidence_record_free(first); evidence_record_free(second);
person_evidence_selection_free(selection);
}
static void test_duplicates(void)
{
PersonEvidenceSelection *selection = person_evidence_selection_new();
EvidenceRecord *first = record(
"10000000-0000-4000-8000-000000000001", "SPECIMEN.png",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
GError *error = NULL;
g_assert_true(person_evidence_selection_add_existing(
selection, first, &error));
g_assert_false(person_evidence_selection_add_existing(
selection, first, &error));
g_assert_error(error,
g_quark_from_static_string("person-evidence-selection-error"), 2);
g_clear_error(&error);
g_assert_false(person_evidence_selection_add_staged(selection,
"/tmp/SPECIMEN-copy", "/tmp/SPECIMEN-stage", "copy",
"image/png", "photo", 12,
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
NULL, "2026-07-28T10:00:00Z", &error));
g_assert_nonnull(strstr(error->message, "déjà"));
g_clear_error(&error);
g_assert_true(person_evidence_selection_remove(selection,
person_evidence_selection_item_get_identifier(
person_evidence_selection_get(selection, 0))));
g_assert_true(person_evidence_selection_add_existing(
selection, first, &error));
evidence_record_free(first);
person_evidence_selection_free(selection);
}
static void test_deep_copy_survives_sources(void)
{
PersonEvidenceSelection *source = person_evidence_selection_new();
EvidenceRecord *incomplete = record(
"10000000-0000-4000-8000-000000000003", "SPECIMEN-NULL.jpg",
"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd");
evidence_record_set_display_metadata(incomplete, NULL, NULL);
g_assert_true(person_evidence_selection_add_existing(
source, incomplete, NULL));
g_assert_true(person_evidence_selection_set_type(source,
person_evidence_selection_item_get_identifier(
person_evidence_selection_get(source, 0)), "email"));
PersonEvidenceSelection *copy = person_evidence_selection_copy(source);
evidence_record_free(incomplete);
person_evidence_selection_free(source);
g_assert_cmpuint(person_evidence_selection_get_count(copy), ==, 1);
const PersonEvidenceSelectionItem *item =
person_evidence_selection_get(copy, 0);
g_assert_cmpstr(person_evidence_selection_item_get_original_name(item),
==, "SPECIMEN-NULL.jpg");
g_assert_cmpstr(person_evidence_selection_item_get_type_identifier(item),
==, "email");
g_assert_true(person_evidence_selection_remove(copy,
person_evidence_selection_item_get_identifier(item)));
person_evidence_selection_free(copy);
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
g_test_add_func("/person-evidence/collection", test_collection);
g_test_add_func("/person-evidence/duplicates", test_duplicates);
g_test_add_func("/person-evidence/deep-copy",
test_deep_copy_survives_sources);
return g_test_run();
}