feat(person): ajouter le parcours multi-preuves et les aperçus partagés
This commit is contained in:
parent
dfe6afcfca
commit
b0ae90090a
48 changed files with 4103 additions and 627 deletions
78
Makefile
78
Makefile
|
|
@ -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) \
|
||||
|
|
|
|||
|
|
@ -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 d’une 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 qu’aprè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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,25 @@
|
|||
# Architecture
|
||||
|
||||
`EvidencePreviewWidget` est l’adaptateur GTK unique de `EvidencePreview` pour
|
||||
`CreatePersonDialog` et la fiche directe. Il encapsule les états, le rendu
|
||||
EML/PDF/image/texte/vidéo, l’annulation, la génération, la garde de session et
|
||||
l’arrê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 l’empreinte 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. L’aperç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
|
||||
l’inventaire 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 d’une 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 l’empreinte de la source et de sa copie, détecte le MIME
|
||||
et nettoie les temporaires lors d’un retrait ou d’une 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 d’une autre enquête.
|
||||
|
||||
Les aperçus PNG/JPEG utilisent exclusivement le fichier interne d’une 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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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 l’extension. 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 d’abord une copie de staging
|
||||
temporaire vérifiée. Le fichier source n’est ni renommé ni modifié et ne doit
|
||||
plus être relu pour l’aperçu ou l’import final après préparation.
|
||||
|
||||
### 7.2 Immutabilité des preuves originales
|
||||
|
||||
Une preuve originale n'est jamais modifiée.
|
||||
|
|
|
|||
|
|
@ -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 n’est lancé par
|
||||
l’aperçu et aucun dérivé n’est 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 n’est lancé et aucun dérivé
|
||||
n’est 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 d’un nouvel outil.
|
||||
|
|
|
|||
|
|
@ -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 d’aperç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 d’orientation libheif restent
|
||||
appliquées par le décodage standard ; seule l’image principale est rendue et
|
||||
le nombre d’images 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
|
||||
|
|
|
|||
|
|
@ -1,13 +1,21 @@
|
|||
# Roadmap
|
||||
|
||||
La tranche 2 partage désormais l’aperçu entre l’assistant 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 n’inclut aucun OCR.
|
||||
|
||||
L’aperç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 d’une preuve existante, la création transactionnelle et la garde de
|
||||
session. Import intégré, PDF, vidéo, EML avancé, OCR et documents d’identité
|
||||
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 l’import multiple
|
||||
différé, ainsi que la création transactionnelle avec compensation des
|
||||
fichiers. OCR, extraction de documents d’identité et analyses documentaires
|
||||
avancées restent hors périmètre.
|
||||
> **Usage opérationnel :** non prêt pour la production
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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 qu’un 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`.
|
||||
|
||||
|
|
|
|||
73
docs/TEST_MANUEL_TICKET_109_TRANCHE_2.md
Normal file
73
docs/TEST_MANUEL_TICKET_109_TRANCHE_2.md
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# Test manuel — ticket #109, tranche 2
|
||||
|
||||
Comparer le même EML synthétique dans la fiche directe et dans l’assistant :
|
||||
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 l’ouverture d’une fiche personne.
|
||||
Vérifier l’absence de critique GTK, d’écriture SQLite et d’OCR.
|
||||
|
||||
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 qu’un 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 n’apparaî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 d’indisponibilité 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 l’enquê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 l’unicité 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 l’absence 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 l’application.
|
||||
13. Recommencer et changer d’enquête pendant une préparation ou une création.
|
||||
Vérifier l’annulation, l’absence d’écriture dans la nouvelle enquête et
|
||||
l’absence de rafraîchissement tardif.
|
||||
14. Confirmer normalement. Vérifier que toutes les preuves sont rattachées à
|
||||
la personne avec une source `manual`, puis rouvrir l’enquête.
|
||||
15. Comparer les empreintes des fichiers sources avant et après le parcours.
|
||||
Elles doivent être identiques.
|
||||
16. Vérifier qu’aucun OCR ni outil documentaire avancé n’a é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 l’absence 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 l’aperçu apparaît et que « MIME détecté : image/jpeg » est
|
||||
affiché sans modification SQLite.
|
||||
19. Renommer un fichier texte synthétique avec l’extension `.jpg`, puis
|
||||
vérifier que l’aperçu est refusé malgré l’extension. Refaire avec un MIME
|
||||
historique `image/jpeg` incohérent et vérifier le même refus.
|
||||
20. Modifier le type individuel de l’image 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 l’image, 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 l’arrêt vidéo.
|
||||
24. Fermer puis changer d’enquête pendant un aperçu ; vérifier l’annulation.
|
||||
25. Vérifier les empreintes inchangées et l’absence d’OCR, d’accès distant et
|
||||
d’écriture SQLite pendant l’aperç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.
|
||||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
35
include/core/evidence_staging.h
Normal file
35
include/core/evidence_staging.h
Normal 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
|
||||
14
include/core/evidence_staging_task.h
Normal file
14
include/core/evidence_staging_task.h
Normal 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
|
||||
25
include/core/evidence_video_preview_controller.h
Normal file
25
include/core/evidence_video_preview_controller.h
Normal 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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
24
include/core/person_creation_coordinator.h
Normal file
24
include/core/person_creation_coordinator.h
Normal 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
|
||||
20
include/core/person_creation_task.h
Normal file
20
include/core/person_creation_task.h
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
87
include/models/person_evidence_selection.h
Normal file
87
include/models/person_evidence_selection.h
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
|||
30
include/widgets/evidence_preview_widget.h
Normal file
30
include/widgets/evidence_preview_widget.h
Normal 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
|
||||
|
|
@ -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é.
|
||||
|
|
|
|||
|
|
@ -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,19 +5828,8 @@ 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))
|
||||
{
|
||||
if (investigation_root != NULL) {
|
||||
const char *selected_name =
|
||||
evidence_record_get_original_name(evidence_record);
|
||||
char *lower_name = selected_name != NULL
|
||||
|
|
@ -5795,10 +5837,8 @@ static void application_on_evidence_selected(
|
|||
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));
|
||||
}
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
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;
|
||||
}
|
||||
width = cairo_image_surface_get_width(surface);
|
||||
height = cairo_image_surface_get_height(surface);
|
||||
if (width > EVIDENCE_PREVIEW_MAX_DIMENSION ||
|
||||
height > EVIDENCE_PREVIEW_MAX_DIMENSION ||
|
||||
(guint64) width * (guint64) height > EVIDENCE_PREVIEW_MAX_PIXELS) {
|
||||
g_set_error_literal(error, preview_error(), 5,
|
||||
"Les dimensions de l'image dépassent les limites sûres.");
|
||||
cairo_surface_destroy(surface);
|
||||
goto cleanup;
|
||||
}
|
||||
surface_to_result(surface, width, height, request, &result, error);
|
||||
cairo_surface_destroy(surface);
|
||||
goto cleanup;
|
||||
}
|
||||
FILE *jpeg_file = fopen(path, "rb");
|
||||
struct jpeg_decompress_struct jpeg = {0};
|
||||
PreviewJpegError jpeg_error;
|
||||
cairo_surface_t *surface = NULL;
|
||||
if (jpeg_file == NULL) {
|
||||
g_set_error_literal(error, preview_error(), 4,
|
||||
"Le fichier image est illisible ou corrompu.");
|
||||
goto cleanup;
|
||||
}
|
||||
jpeg.err = jpeg_std_error(&jpeg_error.base);
|
||||
jpeg_error.base.error_exit = jpeg_failed;
|
||||
if (setjmp(jpeg_error.jump) != 0) {
|
||||
jpeg_destroy_decompress(&jpeg); fclose(jpeg_file);
|
||||
if (surface != NULL) cairo_surface_destroy(surface);
|
||||
g_set_error_literal(error, preview_error(), 4,
|
||||
"Le fichier JPEG est illisible ou corrompu.");
|
||||
goto cleanup;
|
||||
}
|
||||
jpeg_create_decompress(&jpeg); jpeg_stdio_src(&jpeg, jpeg_file);
|
||||
jpeg_read_header(&jpeg, TRUE);
|
||||
width = (gint) jpeg.image_width; height = (gint) jpeg.image_height;
|
||||
if (width > EVIDENCE_PREVIEW_MAX_DIMENSION ||
|
||||
height > EVIDENCE_PREVIEW_MAX_DIMENSION ||
|
||||
(guint64) width * (guint64) height > EVIDENCE_PREVIEW_MAX_PIXELS) {
|
||||
g_set_error_literal(error, preview_error(), 5,
|
||||
"Les dimensions de l'image dépassent les limites sûres.");
|
||||
jpeg_destroy_decompress(&jpeg); fclose(jpeg_file);
|
||||
goto cleanup;
|
||||
}
|
||||
jpeg.out_color_space = JCS_RGB;
|
||||
jpeg_start_decompress(&jpeg);
|
||||
surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24, width, height);
|
||||
unsigned char *pixels = cairo_image_surface_get_data(surface);
|
||||
gint stride = cairo_image_surface_get_stride(surface);
|
||||
JSAMPARRAY row = (*jpeg.mem->alloc_sarray)(
|
||||
(j_common_ptr) &jpeg, JPOOL_IMAGE, (JDIMENSION) width * 3U, 1);
|
||||
while (jpeg.output_scanline < jpeg.output_height) {
|
||||
jpeg_read_scanlines(&jpeg, row, 1);
|
||||
guint y = jpeg.output_scanline - 1;
|
||||
guint32 *destination = (guint32 *) (pixels + y * stride);
|
||||
for (gint x = 0; x < width; x++)
|
||||
destination[x] = ((guint32) row[0][x * 3] << 16) |
|
||||
((guint32) row[0][x * 3 + 1] << 8) | row[0][x * 3 + 2];
|
||||
if (cancellable != NULL && g_cancellable_is_cancelled(cancellable))
|
||||
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;
|
||||
}
|
||||
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
194
src/core/evidence_staging.c
Normal 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,
|
||||
©_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(©_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);
|
||||
}
|
||||
66
src/core/evidence_staging_task.c
Normal file
66
src/core/evidence_staging_task.c
Normal 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;
|
||||
}
|
||||
50
src/core/evidence_video_preview_controller.c
Normal file
50
src/core/evidence_video_preview_controller.c
Normal 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; }
|
||||
|
|
@ -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 d’identification : %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 n’a encore été effectuée.\n"
|
||||
"L’annulation 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);
|
||||
}
|
||||
|
|
|
|||
276
src/core/person_creation_coordinator.c
Normal file
276
src/core/person_creation_coordinator.c
Normal 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 l’assistant de création d’une 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);
|
||||
}
|
||||
98
src/core/person_creation_task.c
Normal file
98
src/core/person_creation_task.c
Normal 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 d’ouvrir la base de l’enquê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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
"L’empreinte 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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
295
src/models/person_evidence_selection.c
Normal file
295
src/models/person_evidence_selection.c
Normal 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)
|
||||
|
|
@ -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 d’abord 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 : l’enquê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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
346
src/widgets/evidence_preview_widget.c
Normal file
346
src/widgets/evidence_preview_widget.c
Normal 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é : l’enquê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 l’aperç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 l’aperç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);
|
||||
}
|
||||
|
|
@ -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 d’ouvrir 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 l’aperçu agrandi lorsqu’un 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 l’aperç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;
|
||||
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");
|
||||
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;
|
||||
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 || !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);
|
||||
goto text_preview_ready;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
g_free(contents);
|
||||
}
|
||||
message = g_strdup_printf(
|
||||
"Aperçu intégré indisponible pour %s%s%s. Utilisez le bouton "
|
||||
"pour ouvrir le document avec l’application 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,
|
||||
|
|
|
|||
|
|
@ -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 n’a 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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
84
tests/test_evidence_preview_widget_gtk.c
Normal file
84
tests/test_evidence_preview_widget_gtk.c
Normal 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();
|
||||
}
|
||||
70
tests/test_evidence_staging.c
Normal file
70
tests/test_evidence_staging.c
Normal 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();
|
||||
}
|
||||
41
tests/test_evidence_video_preview_controller.c
Normal file
41
tests/test_evidence_video_preview_controller.c
Normal 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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
114
tests/test_person_creation_coordinator.c
Normal file
114
tests/test_person_creation_coordinator.c
Normal 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();
|
||||
}
|
||||
117
tests/test_person_evidence_selection.c
Normal file
117
tests/test_person_evidence_selection.c
Normal 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();
|
||||
}
|
||||
Loading…
Reference in a new issue