Compare commits

..

No commits in common. "db9be54a1a39a4a415f2e53890f7df65765596ca" and "8fcd6b0e0d61b8872e47c00a7e49353e2b9a0c65" have entirely different histories.

64 changed files with 587 additions and 8351 deletions

5
.gitignore vendored
View file

@ -17,8 +17,3 @@ labfy-investigation
.cache/
compile_commands.json
/AGENTS.md
/prompt.md
# OpenCode local
/opencode.json
/.opencode/

104
Makefile
View file

@ -126,7 +126,6 @@ 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_EML_ANALYZER := tests/test_eml_analyzer
TEST_EML_INTEGRATION := tests/test_eml_integration
TEST_IBAN_ANALYZER := tests/test_iban_analyzer
TEST_EXIFTOOL_METADATA := tests/test_exiftool_metadata
TEST_PDF_PASSWORD_RECOVERY := tests/test_pdf_password_recovery
@ -136,31 +135,12 @@ TEST_RELATION_TYPE_SERVICE := tests/test_relation_type_service
TEST_CONTROLLED_VOCAB := tests/test_controlled_vocab
TEST_BANK_PROPOSAL := tests/test_bank_proposal
TEST_EML_PIPELINE_TASK := tests/test_eml_pipeline_task
TEST_EML_MIME_EXTRACTOR := tests/test_eml_mime_extractor
TEST_EXIFTOOL_ANALYSIS := tests/test_exiftool_analysis
TEST_OCR_ANALYSIS := tests/test_ocr_analysis
TEST_PDF_ANALYSIS := tests/test_pdf_analysis
TEST_DOCUMENT_TOOL_RUNNER := tests/test_document_tool_runner
FAKE_DOCUMENT_TOOL := tests/fake_document_tool
DOCUMENT_ANALYSIS_TEST_SOURCES := \
src/core/document_analysis.c \
src/core/document_tool_runner.c \
src/core/tool_process.c \
src/core/file_hash.c
$(TEST_DOCUMENT_TOOL_RUNNER): tests/test_document_tool_runner.c \
$(DOCUMENT_ANALYSIS_TEST_SOURCES) $(FAKE_DOCUMENT_TOOL)
$(CC) $(TEST_CFLAGS) -Wpedantic \
tests/test_document_tool_runner.c \
$(DOCUMENT_ANALYSIS_TEST_SOURCES) -o $@ $(TEST_LDFLAGS)
all: $(TARGET)
$(TEST_BANK_PROPOSAL): \
tests/test_bank_proposal.c \
src/core/bank_proposal.c \
src/core/iban_analyzer.c \
src/core/controlled_vocab.c
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ \
$(shell $(PKG_CONFIG) --libs glib-2.0)
@ -176,52 +156,9 @@ $(TEST_EML_PIPELINE_TASK): \
src/core/eml_pipeline_task.c src/core/eml_mime_extractor.c \
src/core/eml_analyzer.c src/core/bank_proposal.c \
src/core/controlled_vocab.c src/core/iban_analyzer.c \
src/core/file_hash.c src/core/background_task.c \
src/core/document_analysis.c src/core/document_tool_runner.c \
src/core/exiftool_analysis.c src/core/ocr_analysis.c \
src/core/pdf_analysis.c src/core/document_file_analysis.c \
src/core/tool_process.c $(FAKE_DOCUMENT_TOOL)
$(CC) $(TEST_CFLAGS) -Wpedantic \
tests/test_eml_pipeline_task.c \
src/core/eml_pipeline_task.c src/core/eml_mime_extractor.c \
src/core/eml_analyzer.c src/core/bank_proposal.c \
src/core/controlled_vocab.c src/core/iban_analyzer.c \
src/core/file_hash.c src/core/background_task.c \
src/core/document_analysis.c src/core/document_tool_runner.c \
src/core/exiftool_analysis.c src/core/ocr_analysis.c \
src/core/pdf_analysis.c src/core/document_file_analysis.c \
src/core/tool_process.c -o $@ $(TEST_LDFLAGS) -lsqlite3
$(TEST_EML_MIME_EXTRACTOR): \
tests/test_eml_mime_extractor.c \
src/core/eml_mime_extractor.c \
src/core/file_hash.c
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ $(TEST_LDFLAGS)
$(FAKE_DOCUMENT_TOOL): tests/fake_document_tool.c
$(CC) -std=c17 -Wall -Wextra -Werror -Wpedantic $< -o $@
$(TEST_EXIFTOOL_ANALYSIS): tests/test_exiftool_analysis.c \
src/core/exiftool_analysis.c $(DOCUMENT_ANALYSIS_TEST_SOURCES) \
$(FAKE_DOCUMENT_TOOL)
$(CC) $(TEST_CFLAGS) -Wpedantic \
tests/test_exiftool_analysis.c src/core/exiftool_analysis.c \
$(DOCUMENT_ANALYSIS_TEST_SOURCES) -o $@ $(TEST_LDFLAGS)
$(TEST_OCR_ANALYSIS): tests/test_ocr_analysis.c \
src/core/ocr_analysis.c $(DOCUMENT_ANALYSIS_TEST_SOURCES) \
$(FAKE_DOCUMENT_TOOL)
$(CC) $(TEST_CFLAGS) -Wpedantic \
tests/test_ocr_analysis.c src/core/ocr_analysis.c \
$(DOCUMENT_ANALYSIS_TEST_SOURCES) -o $@ $(TEST_LDFLAGS)
$(TEST_PDF_ANALYSIS): tests/test_pdf_analysis.c \
src/core/pdf_analysis.c src/core/ocr_analysis.c \
$(DOCUMENT_ANALYSIS_TEST_SOURCES) $(FAKE_DOCUMENT_TOOL)
$(CC) $(TEST_CFLAGS) -Wpedantic \
tests/test_pdf_analysis.c src/core/pdf_analysis.c \
src/core/ocr_analysis.c $(DOCUMENT_ANALYSIS_TEST_SOURCES) \
-o $@ $(TEST_LDFLAGS)
src/core/rib_ocr.c src/core/file_hash.c src/core/background_task.c
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ \
$(TEST_LDFLAGS) -lsqlite3
@ -317,7 +254,6 @@ $(TEST_EXTRACTION_DROP_SERVICE): \
src/dao/entity_dao.c \
src/dao/evidence_entity_dao.c \
src/models/evidence_record.c \
src/models/evidence_observation.c \
src/models/entity_record.c \
src/database/database.c \
src/database/schema.c \
@ -577,7 +513,6 @@ $(TEST_EVIDENCE_ENTITY_DAO): \
src/dao/entity_dao.c \
src/models/evidence_record.c \
src/models/entity_record.c \
src/models/evidence_observation.c \
src/database/database.c \
src/database/schema.c \
src/database/statement.c \
@ -762,7 +697,6 @@ $(TEST_SOCIAL_ACCOUNT_SERVICE): \
src/dao/entity_dao.c \
src/dao/evidence_entity_dao.c \
src/models/entity_record.c \
src/models/evidence_observation.c \
src/database/database.c \
src/database/schema.c \
src/database/statement.c \
@ -781,7 +715,6 @@ $(TEST_PERSON_ENTITY_SERVICE): \
src/dao/entity_dao.c \
src/dao/evidence_entity_dao.c \
src/models/entity_record.c \
src/models/evidence_observation.c \
src/database/database.c \
src/database/schema.c \
src/database/statement.c \
@ -792,14 +725,6 @@ $(TEST_PERSON_ENTITY_SERVICE): \
$(TEST_EML_ANALYZER): tests/test_eml_analyzer.c src/core/eml_analyzer.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
$(TEST_EML_INTEGRATION): tests/test_eml_integration.c \
src/core/eml_integration.c src/core/controlled_vocab.c \
src/dao/entity_dao.c src/dao/evidence_entity_dao.c \
src/models/entity_record.c src/models/evidence_observation.c \
src/database/database.c src/database/schema.c \
src/database/statement.c src/database/transaction.c src/database/error.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -lsqlite3
$(TEST_IBAN_ANALYZER): tests/test_iban_analyzer.c src/core/iban_analyzer.c
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
@ -896,7 +821,6 @@ test: \
$(TEST_SOCIAL_PLATFORM) \
$(TEST_PERSON_ENTITY_SERVICE) \
$(TEST_EML_ANALYZER) \
$(TEST_EML_INTEGRATION) \
$(TEST_IBAN_ANALYZER) \
$(TEST_EXIFTOOL_METADATA) \
$(TEST_PDF_PASSWORD_RECOVERY) \
@ -905,12 +829,7 @@ test: \
$(TEST_RELATION_TYPE_SERVICE) \
$(TEST_CONTROLLED_VOCAB) \
$(TEST_BANK_PROPOSAL) \
$(TEST_EML_PIPELINE_TASK) \
$(TEST_EML_MIME_EXTRACTOR) \
$(TEST_EXIFTOOL_ANALYSIS) \
$(TEST_OCR_ANALYSIS) \
$(TEST_PDF_ANALYSIS) \
$(TEST_DOCUMENT_TOOL_RUNNER)
$(TEST_EML_PIPELINE_TASK)
@echo "Exécution des tests..."
@./$(TEST_NODE)
@./$(TEST_TREE_MODEL)
@ -970,7 +889,6 @@ test: \
@$(TEST_SOCIAL_PLATFORM)
@$(TEST_PERSON_ENTITY_SERVICE)
@$(TEST_EML_ANALYZER)
@$(TEST_EML_INTEGRATION)
@$(TEST_IBAN_ANALYZER)
@$(TEST_EXIFTOOL_METADATA)
@$(TEST_PDF_PASSWORD_RECOVERY)
@ -980,11 +898,6 @@ test: \
@$(TEST_CONTROLLED_VOCAB)
@$(TEST_BANK_PROPOSAL)
@$(TEST_EML_PIPELINE_TASK)
@$(TEST_EML_MIME_EXTRACTOR)
@$(TEST_EXIFTOOL_ANALYSIS)
@$(TEST_OCR_ANALYSIS)
@$(TEST_PDF_ANALYSIS)
@$(TEST_DOCUMENT_TOOL_RUNNER)
@echo "Tous les tests sont valides."
%.o: %.c
@ -1051,19 +964,12 @@ clean:
$(TEST_SOCIAL_PLATFORM) \
$(TEST_PERSON_ENTITY_SERVICE) \
$(TEST_EML_ANALYZER) \
$(TEST_EML_INTEGRATION) \
$(TEST_EXTRACTION_DROP_SERVICE) \
$(TEST_RELATION_TYPE_NORMALIZER) \
$(TEST_RELATION_TYPE_SERVICE) \
$(TEST_CONTROLLED_VOCAB) \
$(TEST_BANK_PROPOSAL) \
$(TEST_EML_PIPELINE_TASK) \
$(TEST_EML_MIME_EXTRACTOR) \
$(TEST_EXIFTOOL_ANALYSIS) \
$(TEST_OCR_ANALYSIS) \
$(TEST_PDF_ANALYSIS) \
$(TEST_DOCUMENT_TOOL_RUNNER) \
$(FAKE_DOCUMENT_TOOL)
$(TEST_EML_PIPELINE_TASK)

View file

@ -180,37 +180,6 @@ Le socle actuel comprend notamment :
- glisser-déposer des extractions texte depuis l'arborescence vers le graphe,
avec confirmation explicite, création d'entité ou rattachement à une entité
existante, sans déplacement du fichier produit ;
- pivot forensique EML : contrôle SHA-256 avant analyse, lecture des en-têtes,
extraction MIME récursive, inventaire des pièces jointes et analyse locale
facultative par PDF, OCR et ExifTool ;
- propositions bancaires issues du texte ou de l'OCR, avec validation IBAN/BIC
avant toute conservation ;
- observations EML persistantes dans la fiche de preuve, sans création
automatique d'un nœud ;
- promotion facultative et explicite d'une observation vers une entité du
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.
### Pivot EML
L'action « Analyser l'e-mail » vérifie d'abord que l'empreinte SHA-256 de la
preuve correspond à celle enregistrée. Le pipeline lit ensuite les en-têtes,
parcourt la structure MIME et présente les pièces jointes, textes PDF ou OCR,
métadonnées ExifTool et propositions bancaires disponibles.
Une proposition cochée avec « Conserver dans la fiche » devient une
observation persistante liée à la preuve. Cette confirmation normale ne crée
ni entité, ni nœud, ni rattachement `preuve_entites`. « Promouvoir en entité »
est une décision séparée, facultative et désactivée par défaut. Une promotion
peut ensuite être retirée du graphe sans supprimer l'observation ; une entité
encore utilisée par une autre observation, une autre preuve ou une relation
est conservée.
Les outils documentaires sont optionnels : leur absence produit un résultat
partiel sans empêcher la lecture des en-têtes ni l'extraction MIME. Les
métadonnées sensibles, notamment GPS, sont signalées et ne sont jamais
promues automatiquement.
Les outils actuellement présents dans le catalogue initial sont :
@ -316,7 +285,7 @@ Les paquets AUR ne devront jamais devenir une dépendance obligatoire du futur p
Depuis la racine du dépôt :
```bash
make -j8
make
```
Le binaire produit est :
@ -346,15 +315,15 @@ Le projet est compilé en C17 avec les avertissements traités comme des erreurs
Lancer tous les tests :
```bash
make -j8 test
make test
```
Vérifications recommandées avant chaque commit :
```bash
make clean
make -j8
make -j8 test
make
make test
git diff --check
```

View file

@ -177,64 +177,3 @@ INSERT OR IGNORE INTO relation_types(code, label, normalized_key, description, i
('held_at', 'Tenu auprès de', 'tenu auprès de', 'Compte bancaire ouvert dans une banque.', 1),
('named_as_holder_of', 'Nommé titulaire de', 'nommé titulaire de', 'Personne ou entité observée comme titulaire du RIB.', 1),
('supports', 'Soutient', 'soutient', 'Preuve soutenant une entité ou relation.', 1);
CREATE TABLE IF NOT EXISTS evidence_entity_observations
(
id TEXT PRIMARY KEY,
evidence_id TEXT NOT NULL,
entity_id TEXT,
entity_type TEXT NOT NULL,
value_raw TEXT NOT NULL,
value_normalized TEXT,
value_corrected TEXT,
role TEXT NOT NULL,
provenance_kind TEXT NOT NULL,
source_header TEXT NOT NULL,
occurrence INTEGER NOT NULL DEFAULT 1 CHECK (occurrence > 0),
verification_status TEXT NOT NULL DEFAULT 'proposed',
extraction_id TEXT,
warning TEXT,
observed_at TEXT NOT NULL CHECK (length(observed_at) = 20),
integrated_at TEXT NOT NULL CHECK (length(integrated_at) = 20),
promoted_at TEXT,
promotion_kind TEXT,
UNIQUE (evidence_id, entity_type, value_normalized, role, source_header,
occurrence, provenance_kind, extraction_id),
FOREIGN KEY (evidence_id) REFERENCES preuves(id) ON DELETE CASCADE,
FOREIGN KEY (entity_id) REFERENCES entites(id) ON DELETE SET NULL,
FOREIGN KEY (extraction_id) REFERENCES extractions(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_evidence_entity_observations_evidence
ON evidence_entity_observations(evidence_id);
CREATE INDEX IF NOT EXISTS idx_evidence_entity_observations_entity
ON evidence_entity_observations(entity_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_evidence_entity_observations_semantic
ON evidence_entity_observations(
evidence_id,entity_type,value_normalized,role,source_header,
occurrence,provenance_kind,COALESCE(extraction_id,''));
CREATE TABLE IF NOT EXISTS preuve_entite_sources
(
id TEXT PRIMARY KEY,
preuve_id TEXT NOT NULL,
entite_id TEXT NOT NULL,
source_kind TEXT NOT NULL CHECK (
source_kind IN ('manual', 'legacy_manual', 'eml_observation')
),
source_uuid TEXT,
created_at TEXT NOT NULL CHECK (length(created_at) = 20),
FOREIGN KEY (preuve_id, entite_id)
REFERENCES preuve_entites(preuve_id, entite_id) ON DELETE CASCADE,
CHECK (
(source_kind = 'eml_observation' AND source_uuid IS NOT NULL) OR
(source_kind <> 'eml_observation' AND source_uuid IS NULL)
)
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_preuve_entite_sources_unique
ON preuve_entite_sources(
preuve_id, entite_id, source_kind, COALESCE(source_uuid, '')
);
CREATE INDEX IF NOT EXISTS idx_preuve_entite_sources_entity
ON preuve_entite_sources(entite_id);
CREATE INDEX IF NOT EXISTS idx_preuve_entite_sources_source
ON preuve_entite_sources(source_kind, source_uuid);

View file

@ -1,24 +0,0 @@
/******************************************************************************
* Schéma SQLite V11 observations sémantiques preuve-entité.
******************************************************************************/
CREATE TABLE IF NOT EXISTS evidence_entity_observations
(
evidence_id TEXT NOT NULL,
entity_id TEXT NOT NULL,
entity_type TEXT NOT NULL,
value_raw TEXT NOT NULL,
value_normalized TEXT NOT NULL,
role TEXT NOT NULL,
provenance_kind TEXT NOT NULL,
source_header TEXT NOT NULL,
occurrence INTEGER NOT NULL DEFAULT 1 CHECK (occurrence > 0),
verification_status TEXT NOT NULL DEFAULT 'proposed',
created_at TEXT NOT NULL CHECK (length(created_at) = 20),
PRIMARY KEY (evidence_id, entity_id, role, source_header, occurrence),
FOREIGN KEY (evidence_id) REFERENCES preuves(id) ON DELETE CASCADE,
FOREIGN KEY (entity_id) REFERENCES entites(id) ON DELETE CASCADE,
CHECK (length(trim(role)) > 0),
CHECK (length(trim(source_header)) > 0)
);
CREATE INDEX IF NOT EXISTS idx_evidence_entity_observations_evidence
ON evidence_entity_observations(evidence_id);

View file

@ -1,55 +0,0 @@
/******************************************************************************
* Schéma SQLite V12 observations indépendantes et promotion facultative.
******************************************************************************/
ALTER TABLE evidence_entity_observations RENAME TO evidence_entity_observations_v11;
CREATE TABLE evidence_entity_observations
(
id TEXT PRIMARY KEY,
evidence_id TEXT NOT NULL,
entity_id TEXT,
entity_type TEXT NOT NULL,
value_raw TEXT NOT NULL,
value_normalized TEXT,
value_corrected TEXT,
role TEXT NOT NULL,
provenance_kind TEXT NOT NULL,
source_header TEXT NOT NULL,
occurrence INTEGER NOT NULL DEFAULT 1 CHECK (occurrence > 0),
extraction_id TEXT,
verification_status TEXT NOT NULL DEFAULT 'proposed',
warning TEXT,
observed_at TEXT NOT NULL CHECK (length(observed_at) = 20),
integrated_at TEXT NOT NULL CHECK (length(integrated_at) = 20),
promoted_at TEXT CHECK (promoted_at IS NULL OR length(promoted_at) = 20),
promotion_kind TEXT CHECK (promotion_kind IS NULL OR promotion_kind IN ('created', 'reused', 'legacy')),
FOREIGN KEY (evidence_id) REFERENCES preuves(id) ON DELETE CASCADE,
FOREIGN KEY (entity_id) REFERENCES entites(id) ON DELETE SET NULL,
FOREIGN KEY (extraction_id) REFERENCES extractions(id) ON DELETE SET NULL,
UNIQUE (evidence_id, entity_type, value_normalized, role, source_header,
occurrence, provenance_kind, extraction_id)
);
INSERT INTO evidence_entity_observations(
id,evidence_id,entity_id,entity_type,value_raw,value_normalized,role,
provenance_kind,source_header,occurrence,verification_status,
observed_at,integrated_at,promoted_at,promotion_kind)
SELECT
lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-4' ||
substr(lower(hex(randomblob(2))),2) || '-a' ||
substr(lower(hex(randomblob(2))),2) || '-' || lower(hex(randomblob(6))),
evidence_id,entity_id,entity_type,value_raw,value_normalized,role,
provenance_kind,source_header,occurrence,verification_status,
created_at,created_at,created_at,'legacy'
FROM evidence_entity_observations_v11;
DROP TABLE evidence_entity_observations_v11;
CREATE INDEX idx_evidence_entity_observations_evidence
ON evidence_entity_observations(evidence_id);
CREATE INDEX idx_evidence_entity_observations_entity
ON evidence_entity_observations(entity_id);
CREATE UNIQUE INDEX idx_evidence_entity_observations_semantic
ON evidence_entity_observations(
evidence_id,entity_type,value_normalized,role,source_header,
occurrence,provenance_kind,COALESCE(extraction_id,''));

View file

@ -1,54 +0,0 @@
/******************************************************************************
* Schéma SQLite V13 propriété des associations preuve-entité.
******************************************************************************/
CREATE TABLE preuve_entite_sources
(
id TEXT PRIMARY KEY,
preuve_id TEXT NOT NULL,
entite_id TEXT NOT NULL,
source_kind TEXT NOT NULL CHECK (
source_kind IN ('manual', 'legacy_manual', 'eml_observation')
),
source_uuid TEXT,
created_at TEXT NOT NULL CHECK (length(created_at) = 20),
FOREIGN KEY (preuve_id, entite_id)
REFERENCES preuve_entites(preuve_id, entite_id) ON DELETE CASCADE,
CHECK (
(source_kind = 'eml_observation' AND source_uuid IS NOT NULL) OR
(source_kind <> 'eml_observation' AND source_uuid IS NULL)
)
);
CREATE UNIQUE INDEX idx_preuve_entite_sources_unique
ON preuve_entite_sources(
preuve_id, entite_id, source_kind, COALESCE(source_uuid, '')
);
CREATE INDEX idx_preuve_entite_sources_entity
ON preuve_entite_sources(entite_id);
CREATE INDEX idx_preuve_entite_sources_source
ON preuve_entite_sources(source_kind, source_uuid);
/* Les rattachements antérieurs sont conservés de façon prudente. */
INSERT INTO preuve_entite_sources(
id, preuve_id, entite_id, source_kind, source_uuid, created_at
)
SELECT
lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-4' ||
substr(lower(hex(randomblob(2))),2) || '-a' ||
substr(lower(hex(randomblob(2))),2) || '-' || lower(hex(randomblob(6))),
preuve_id, entite_id, 'legacy_manual', NULL,
strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
FROM preuve_entites;
/* Une promotion V12 identifiable reçoit aussi sa justification précise. */
INSERT OR IGNORE INTO preuve_entite_sources(
id, preuve_id, entite_id, source_kind, source_uuid, created_at
)
SELECT
lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-4' ||
substr(lower(hex(randomblob(2))),2) || '-a' ||
substr(lower(hex(randomblob(2))),2) || '-' || lower(hex(randomblob(6))),
evidence_id, entity_id, 'eml_observation', id,
COALESCE(promoted_at, integrated_at)
FROM evidence_entity_observations
WHERE entity_id IS NOT NULL;

View file

@ -1,8 +1,8 @@
# Architecture
> **Version :** 3.1
> **Dernière mise à jour :** 2026-07-28
> **Schéma SQLite courant :** V13
> **Version :** 3.0
> **Dernière mise à jour :** 2026-07-24
> **Schéma SQLite courant :** V10
> **Statut :** architecture courante
---
@ -301,7 +301,7 @@ validation du chemin
création de l'arborescence
initialisation transactionnelle de SQLite V13
initialisation transactionnelle de SQLite V10
création de l'identité de l'enquête
@ -492,168 +492,43 @@ sont proches visuellement.
## 13. Pivot EML
### 13.1 Parcours
Statut :
```text
preuve EML
↓ contrôle d'intégrité SHA-256
tâche asynchrone
analyse des en-têtes
extraction MIME récursive vers un staging
outils documentaires optionnels
propositions temporaires
↓ confirmation explicite
observations persistantes dans la fiche
↓ promotion facultative
entités canoniques du graphe
PARTIEL
```
La preuve originale n'est jamais modifiée. Le staging et ses pièces jointes
extraites sont supprimés après rejet, annulation, erreur ou intégration.
L'absence d'ExifTool, Tesseract ou Poppler conserve un résultat partiel :
l'analyse des en-têtes et MIME reste utilisable.
La branche contient des briques d'analyse EML, d'extraction MIME, d'IBAN, OCR,
ExifTool, vocabulaire contrôlé, propositions bancaires et pipeline de tâche.
### 13.2 Responsabilités
Le flux cible est :
- `EmlAnalyzer` lit les en-têtes, conserve leurs occurrences et qualifie les
adresses, domaines et IP.
- `EmlMimeExtractor` parcourt les parties imbriquées, y compris
`message/rfc822`, décode Base64, quoted-printable, RFC 2047 et RFC 2231,
assainit les noms et applique les limites de profondeur, nombre et taille.
- `EmlPipelineTask` orchestre en arrière-plan l'analyse, le staging, les outils
documentaires et les propositions bancaires.
- `DocumentToolRunner` lance les programmes avec `GSubprocess`, sans shell,
draine simultanément `stdout` et `stderr`, borne les sorties et propage
l'annulation.
- les modules ExifTool, OCR et PDF structurent les résultats sans modifier la
source. Le PDF privilégie le texte natif puis utilise l'OCR page par page.
- `BankProposal` conserve les valeurs bancaires détectées, leur normalisation,
leur validation et une éventuelle correction OCR distincte.
- `EmlAnalysisDialog` présente les résultats et collecte séparément la
conservation et la promotion.
- `EmlIntegration`, les DAO et `EvidenceObservation` assurent l'écriture
transactionnelle, la déduplication, la promotion et le retrait.
- `Application`, `MainWindow` et `Workspace` raccordent la tâche au contexte
GTK principal, rafraîchissent le graphe et rechargent la fiche depuis SQLite.
```text
EML original
en-têtes et MIME
pièces jointes dérivées
empreintes et métadonnées
OCR et indicateurs
valeurs brutes + normalisées + dérivées
révision humaine
intégration transactionnelle
```
### 13.3 Proposition, observation et entité
Les valeurs brutes restent immuables.
Une **proposition** est un résultat temporaire. Elle peut être rejetée,
invalidée, corrigée, conservée ou accompagnée d'une demande de promotion.
Une IP de relais SMTP ne doit pas être présentée comme l'adresse personnelle
d'un suspect.
Une **observation** est une information confirmée liée à une preuve. La table
`evidence_entity_observations` conserve son UUID, son type, ses valeurs brute,
normalisée et corrigée éventuelle, son rôle, l'en-tête et son occurrence, sa
provenance, son statut, ses dates et une association facultative à une entité.
Une observation peut donc exister durablement sans nœud de graphe.
Une donnée bancaire observée ne prouve pas l'identité de l'auteur d'une fraude.
Une **entité** est un objet canonique réutilisable de `entites`. Elle n'est
créée ou réutilisée que si « Promouvoir en entité » est explicitement coché.
`preuve_entites` fournit alors le rattachement nécessaire à la projection du
graphe. Une même entité peut servir plusieurs rôles, observations, preuves ou
relations.
### 13.4 MIME et outils documentaires
L'extracteur accepte un EML de 50 Mio au maximum. Il limite une partie décodée
à 8 Mio, le total décodé à 32 Mio, le message à 128 parties, la profondeur à
12 niveaux et un nom produit à 240 octets. Les chemins MIME sont conservés,
les fichiers inline et `Content-ID` sont inventoriés, les traversées de chemin
sont neutralisées et les écritures passent par un temporaire renommé après
succès. Cette prise en charge volontairement bornée ne prétend pas couvrir
l'intégralité des RFC MIME.
Une analyse documentaire accepte au maximum 50 Mio, 8 Mio de `stdout`,
256 Kio de `stderr`, 100 pages PDF et 128 documents par pipeline. Un PDF
chiffré n'est pas contourné. `pdfinfo` inspecte le document, `pdftotext`
fournit en priorité le texte natif et `pdftoppm` rend les pages nécessitant un
OCR. L'ordre des pages, les résultats partiels et la méthode utilisée sont
conservés ; les images temporaires sont nettoyées.
Tesseract reçoit `fra+eng` dans le pipeline. Son texte brut n'est pas corrigé
silencieusement : une correction OCR proposée reste distincte. Les arguments,
la version et l'état de l'exécution documentent la provenance.
ExifTool est appelé en sortie JSON avec les groupes de tags. Les champs connus
sont normalisés et les tags inconnus sont conservés avec leur groupe, leur nom
et leur valeur brute. La version de l'outil est attachée à l'exécution. Les
coordonnées GPS sont marquées sensibles et ne créent jamais automatiquement
une entité.
Les propositions bancaires peuvent contenir IBAN brut et normalisé,
validation MOD-97, BIC, banque, titulaire déclaré, adresse, éléments de RIB et
correction OCR distincte. Une donnée invalide n'est pas intégrable. Un
titulaire déclaré dans un document n'établit ni identité certaine ni
attribution pénale : la donnée reste une proposition puis une observation
tant que l'enquêteur ne choisit pas de la promouvoir.
### 13.5 Migrations V11, V12 et V13
V11 crée le premier modèle `evidence_entity_observations`, où chaque
observation est obligatoirement liée à une entité.
V12 donne un UUID propre à l'observation, rend `entity_id` nullable, ajoute les
valeurs corrigées, l'extraction, les avertissements, les dates d'observation,
d'intégration et de promotion ainsi que `promotion_kind`. Les lignes V11 sont
reprises avec `promotion_kind = 'legacy'`. L'index sémantique assure la
déduplication, y compris lorsque `extraction_id` est nul.
V13 ajoute `preuve_entite_sources`. Chaque rattachement matérialisé possède
une justification `manual`, `legacy_manual` ou `eml_observation`. La migration
protège les lignes historiques par `legacy_manual` et reprend les promotions
V12 identifiables.
### 13.6 Promotion et retrait
La conservation seule écrit l'observation et l'affiche dans la fiche, sans
créer `entites` ni `preuve_entites`. La promotion explicite crée ou réutilise
une entité, l'associe à l'observation et ajoute le rattachement nécessaire.
L'opération est transactionnelle et idempotente.
« Retirer du graphe » efface uniquement la source `eml_observation` portant
l'UUID de l'observation et conserve
l'observation. Le nœud n'est supprimé que si les références connues
(observations, preuves, relations, tags, recherches, chronologie, hypothèses,
OSINT, comptes sociaux et rôles de personne) sont absentes.
### 13.7 Qualification et provenance
Les rôles couvrent `From`, `Sender`, `Reply-To`, `Return-Path`, `To`, `Cc`,
`Bcc`, les relais `Received` et le domaine de `Message-ID`. Une même adresse
peut conserver plusieurs rôles sans multiplier l'entité canonique.
`192.0.2.10` et `198.51.100.20` sont des IP, jamais des domaines ;
`MIME-Version: 1.0` ne produit pas de domaine intégrable.
### 13.8 Asynchronisme et sécurité
`BackgroundTask`, `TaskManager` et `GCancellable` portent l'état, la
progression, l'annulation et la remise du résultat au contexte principal.
Le worker ne manipule aucun widget. Avant de présenter le résultat,
`Application` vérifie que la session attendue est toujours active ; un
changement d'enquête rend le résultat caduc et déclenche le nettoyage.
Tous les arguments externes sont séparés, sans shell. `stdout` et `stderr`
sont drainés en parallèle pour éviter un interblocage. Les processus sont
forcés à terminer lors d'une annulation, puis les répertoires temporaires sont
supprimés.
### 13.9 Limites connues
- la promotion est disponible dans le dialogue d'analyse, pas directement
depuis la fiche ;
- la fiche affiche la valeur canonique et les codes persistés, mais pas encore
la valeur brute distincte ni l'UUID de l'entité associée ;
- le runner borne les sorties et gère l'annulation, mais ne possède pas de
délai maximal autonome ;
- les pièces jointes analysées dans le staging ne sont pas persistées comme
dérivés confirmés par ce parcours ;
- les rattachements antérieurs à V13 restent volontairement protégés par
`legacy_manual`, faute de provenance historique plus précise ;
- la couverture visuelle du dialogue et de la fiche reste manuelle.
Le ticket Forgejo #107 reste la référence fonctionnelle du chantier.
---

View file

@ -1,7 +1,7 @@
# Conventions de développement
> **Version :** 2.1
> **Dernière mise à jour :** 2026-07-28
> **Version :** 2.0
> **Dernière mise à jour :** 2026-07-24
> **Projet :** Labfy Investigation
---
@ -265,7 +265,7 @@ présentation graphique.
La version courante du schéma est :
```text
V13
V10
```
Les scripts versionnés sont conservés dans :
@ -273,7 +273,7 @@ Les scripts versionnés sont conservés dans :
```text
database/schema_v1.sql
...
database/schema_v12.sql
database/schema_v10.sql
```
Le complément idempotent du schéma courant est :
@ -614,35 +614,3 @@ HISTORIQUE
La priorité reste la qualité, la traçabilité et la compréhension durable du
projet.
---
## 13. Propositions, observations et entités
Ces objets ne sont pas interchangeables :
- une proposition est temporaire et ne constitue pas un fait persistant ;
- une observation confirmée décrit ce qui a été relevé dans une preuve ;
- une entité est un objet canonique du graphe, créé ou réutilisé uniquement
après une promotion explicite.
Une conservation normale ne crée jamais automatiquement d'entité, de nœud ou
de rattachement `preuve_entites`. La promotion est facultative et désactivée
par défaut.
Une observation conserve obligatoirement une provenance et un rôle
compréhensible dans son contexte. Les codes issus d'un vocabulaire contrôlé
sont utilisés à la place de chaînes libres. Valeur brute, valeur normalisée et
correction utilisateur restent dans des champs distincts ; aucune correction
ne réécrit silencieusement la valeur observée.
Le retrait d'une promotion conserve l'observation et retire exclusivement la
source `eml_observation` identifiée par son UUID. Une entité partagée avec
d'autres observations, preuves ou relations ne doit jamais être supprimée
aveuglément. Le service doit également préserver tout rattachement indépendant
de la promotion. Les sources `manual` et `legacy_manual` persistent
indépendamment et protègent le rattachement matérialisé.
Ces règles s'ajoutent aux interdictions générales : aucune modification d'une
preuve originale, aucun shell dynamique et aucun accès SQLite direct depuis
un widget GTK.

View file

@ -3,9 +3,6 @@
Ce fichier recense les outils utilisés par LabFy Investigation et leur
installation. Il sera complété à chaque intégration dun nouvel outil.
La migration SQLite V13 et la gestion des propriétaires de
`preuve_entites` n'ajoutent aucune dépendance système.
## Principe dinstallation
Les dépendances système sont installées avec le gestionnaire de paquets de la
@ -30,9 +27,8 @@ export PATH="$HOME/.local/share/labfy-osint/bin:$PATH"
| Fonction | Exécutable | Arch Linux (pacman) | Ubuntu/Debian (apt) |
| --- | --- | --- | --- |
| OCR français et anglais | `tesseract` | `sudo pacman -S tesseract tesseract-data-fra tesseract-data-eng` | `sudo apt install tesseract-ocr tesseract-ocr-fra tesseract-ocr-eng` |
| OCR | `tesseract` | `sudo pacman -S tesseract tesseract-data-fra` | `sudo apt install tesseract-ocr tesseract-ocr-fra` |
| Métadonnées | `exiftool` | `sudo pacman -S perl-image-exiftool` | `sudo apt install libimage-exiftool-perl` |
| Inspection, texte et rendu PDF | `pdfinfo`, `pdftotext`, `pdftoppm` | `sudo pacman -S poppler` | `sudo apt install poppler-utils` |
| Recherche DNS | `dig` | `sudo pacman -S bind` | `sudo apt install dnsutils` |
| Résolution réseau | `host` | `sudo pacman -S bind` | `sudo apt install bind9-host` |
| WHOIS | `whois` | `sudo pacman -S whois` | `sudo apt install whois` |
@ -44,7 +40,7 @@ export PATH="$HOME/.local/share/labfy-osint/bin:$PATH"
Après installation, vérifier les outils avec :
```sh
command -v tesseract exiftool pdfinfo pdftotext pdftoppm dig host whois curl openssl qpdf john pdf2john
command -v tesseract exiftool dig host whois curl openssl qpdf john pdf2john
```
## Outils OSINT Python
@ -87,9 +83,8 @@ vérifiée avec `command -v`, puis avec son option de version (`--version` ou
## Notes
- Ne pas installer ces outils dans le dépôt Git.
- ExifTool, Tesseract et Poppler sont optionnels pour le pivot EML. Leur
absence laisse disponibles l'analyse des en-têtes et l'extraction MIME.
- Ne jamais stocker de jetons, mots de passe ou données privées dans ce
fichier.
- Les versions peuvent varier selon la distribution ; noter ici toute
commande particulière nécessaire à Ubuntu des forces de lordre.

View file

@ -1,7 +1,7 @@
# Guide de développement
> **Version :** 2.1
> **Dernière mise à jour :** 2026-07-28
> **Version :** 2.0
> **Dernière mise à jour :** 2026-07-24
> **Projet :** Labfy Investigation
---
@ -86,22 +86,6 @@ système de build ne les impose pas.
Les outils OSINT, OCR, métadonnées ou PDF sont documentés séparément et restent
optionnels sauf décision explicite.
### 3.1 Dépendances documentaires optionnelles
Le pivot EML appelle directement, lorsqu'ils sont installés :
- `exiftool` pour les métadonnées JSON ;
- `tesseract` pour l'OCR, avec la demande `fra+eng` dans le pipeline ;
- `pdfinfo`, `pdftotext` et `pdftoppm` fournis par Poppler pour l'inspection,
le texte natif et le rendu des pages PDF.
Sous Debian ou Ubuntu, les paquets usuels déjà identifiés par le projet sont
`libimage-exiftool-perl`, `tesseract-ocr`, `tesseract-ocr-fra`,
`tesseract-ocr-eng` et `poppler-utils`. Ils ne sont pas nécessaires à la
compilation ni au démarrage. Un exécutable absent produit un état
« indisponible » ou un résultat partiel ; les en-têtes et MIME restent
consultables. L'application ne les installe jamais automatiquement.
---
## 4. Récupération du dépôt
@ -211,42 +195,6 @@ make tests/test_eml_pipeline_task
./tests/test_eml_pipeline_task
```
Tests ciblés du pivot EML :
```sh
make -j8 \
tests/test_eml_analyzer \
tests/test_eml_mime_extractor \
tests/test_eml_pipeline_task \
tests/test_document_tool_runner \
tests/test_exiftool_analysis \
tests/test_ocr_analysis \
tests/test_pdf_analysis \
tests/test_bank_proposal \
tests/test_eml_integration \
tests/test_evidence_entity_dao \
tests/test_database
./tests/test_eml_analyzer
./tests/test_eml_mime_extractor
./tests/test_eml_pipeline_task
./tests/test_document_tool_runner
./tests/test_exiftool_analysis
./tests/test_ocr_analysis
./tests/test_pdf_analysis
./tests/test_bank_proposal
./tests/test_eml_integration
./tests/test_evidence_entity_dao
./tests/test_database
```
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
répertoire temporaire choisi pour le test, importer cette fixture, puis suivre
`docs/testing/EML_PIVOT_MANUAL_TEST.md`. Ne jamais réutiliser une base ou une
preuve réelle.
Le nom exact d'une cible doit être vérifié dans le Makefile.
### 6.3 Validation avant intégration
@ -397,7 +345,7 @@ Lire :
docs/database/DATABASE_ARCHITECTURE.md
docs/database/SCHEMA_AUDIT_CURRENT.md
database/schema_current.sql
database/schema_v12.sql
database/schema_v10.sql
src/database/database.c
src/database/schema.c
tests/test_database.c
@ -405,24 +353,21 @@ tests/test_database.c
### 9.2 Nouvelle version de schéma
Pour créer une nouvelle version après V13, par exemple V14 :
Pour créer V11, par exemple :
1. ajouter `database/schema_v13.sql` ;
2. déclarer et implémenter `schema_install_v13()` ;
3. ajouter `database_migrate_v12_to_v13()` ;
1. ajouter `database/schema_v11.sql` ;
2. déclarer et implémenter `schema_install_v11()` ;
3. ajouter `database_migrate_v10_to_v11()` ;
4. raccorder la migration dans la boucle vers la version courante ;
5. mettre à jour les constantes de version ;
6. installer V13 lors de la création d'une base neuve ;
6. installer V11 lors de la création d'une base neuve ;
7. adapter `schema_current.sql` si nécessaire ;
8. ajouter une fixture V13 vers V14 ;
La fixture V12 vers V13 vérifie le backfill `legacy_manual`. Les tests EML
couvrent aussi le retrait isolé d'une source face à un rattachement manuel.
8. ajouter une fixture V10 vers V11 ;
9. tester une base neuve ;
10. tester le rollback ;
11. vérifier l'intégrité et les clés étrangères ;
12. mettre à jour l'audit courant ;
13. créer l'audit versionné de V13.
13. créer l'audit versionné de V11.
Ne pas réécrire une ancienne migration publiée pour changer sa signification.

View file

@ -1,8 +1,8 @@
# Roadmap
> **Dernière mise à jour :** 2026-07-28
> **Dernière mise à jour :** 2026-07-24
> **État du projet :** développement actif
> **Schéma SQLite courant :** V13
> **Schéma SQLite courant :** V10
> **Usage opérationnel :** non prêt pour la production
---
@ -116,7 +116,7 @@ La branche `main` contient notamment :
- création, validation et ouverture d'enquêtes ;
- session d'enquête remplaçable proprement ;
- infrastructure SQLite et migrations jusqu'à V13 ;
- infrastructure SQLite et migrations jusqu'à V10 ;
- couche Database, DAO et services métier ;
- import de preuves avec copie contrôlée et SHA-256 ;
- vérification d'intégrité et reclassement des preuves ;
@ -135,8 +135,7 @@ La branche `main` contient notamment :
- types canoniques de relations ;
- vocabulaire contrôlé ;
- table V10 `bank_account_entities` ;
- pivot EML raccordé à GTK, observations persistantes indépendantes des
entités, promotion facultative et retrait réversible.
- premiers composants du pipeline EML.
Cette liste est une synthèse et non un contrat de stabilité.
@ -146,13 +145,15 @@ Cette liste est une synthèse et non un contrat de stabilité.
### Ticket #107 — Pivot e-mail forensique
Statut technique :
Statut :
```text
IMPLÉMENTÉ — VALIDATION DOCUMENTAIRE ET CORRECTION BLOQUANTE RESTANTES
PARTIEL
```
Le parcours livré est :
Les briques préparatoires existent, mais le flux complet reste à terminer.
Objectif principal :
```text
preuve EML
@ -170,29 +171,30 @@ OCR et détection d'indicateurs
normalisation sans perte de la valeur brute
interface de révision
↓ conservation explicite
observation persistante dans la fiche
↓ promotion facultative
entité créée ou réutilisée dans le graphe
confirmation explicite
intégration transactionnelle
rafraîchissement des vues et du graphe
```
Les tests automatiques EML, MIME, outils documentaires, PDF, OCR, ExifTool,
banque, intégration et migration sont présents. Le parcours GTK dispose d'une
fixture et d'un guide manuel ; la couverture visuelle demeure manuelle.
Priorités immédiates :
V13 distingue désormais chaque propriétaire du rattachement
`preuve_entites`. Un retrait EML enlève sa seule source
`eml_observation`; les sources manuelles, historiques et celles des autres
observations restent protégées.
1. terminer l'extraction MIME robuste ;
2. conserver les pièces jointes comme fichiers dérivés ;
3. réunir EML, OCR, IBAN et ExifTool dans un pipeline unique ;
4. terminer l'interface de révision ;
5. persister uniquement les propositions confirmées ;
6. réutiliser ou créer les entités correspondantes ;
7. créer les relations canoniques avec leur provenance ;
8. garantir un rollback complet en cas d'échec ;
9. rafraîchir la barre latérale et le graphe ;
10. compléter les tests de migration et d'intégration ;
11. mettre à jour la documentation avec l'état réellement livré.
Améliorations suivantes :
1. proposer la promotion directement depuis la fiche ;
2. afficher les libellés contrôlés, la valeur brute distincte et l'UUID de
l'entité associée ;
3. persister explicitement les dérivés confirmés du staging ;
4. ajouter un délai maximal autonome aux outils documentaires ;
5. automatiser davantage le parcours GTK.
Le ticket reste ouvert tant que son flux complet et ses critères d'acceptation
ne sont pas validés.
---

View file

@ -1,61 +0,0 @@
# Test manuel du parcours EML
Ce parcours complète les tests automatiques. Il utilise exclusivement la
fixture synthétique `tests/fixtures/eml/manual_smoke_test.eml`, dans une
enquête synthétique neuve. Toute donnée ou base réelle est interdite.
## Préparation et tests automatiques
```sh
make -j8
make -j8 test
```
Un échec de compilation ou de test est bloquant. Créer ensuite une enquête
neuve dans un répertoire temporaire dédié, importer la fixture et vérifier son
intégrité depuis la fiche. L'empreinte doit correspondre à celle enregistrée.
## Parcours manuel
| Étape | Action | Résultat attendu | Échec bloquant |
|---|---|---|---|
| 1 | Sélectionner la fixture importée. | « Analyser l'e-mail » est disponible. | Action absente ou active sur un fichier non EML. |
| 2 | Lancer l'analyse puis l'annuler depuis le panneau d'activité. | État annulé, aucun dialogue d'intégration, aucune observation, entité ou relation créée. | Écriture persistante après annulation. |
| 3 | Relancer l'analyse. | Le dialogue affiche en-têtes, deux pièces jointes, textes, métadonnées et avertissements disponibles. | Plantage ou modification de la preuve. |
| 4 | Contrôler `From`, `Sender`, `Reply-To`, `Return-Path`, `To`, `Cc`, `Bcc`, `Message-ID` et chaque `Received`. | Chaque proposition d'en-tête indique son rôle et son origine avec occurrence. | Rôle ou origine perdus. |
| 5 | Contrôler les indicateurs. | `192.0.2.10` et `198.51.100.20` sont des IP ; `1.0` n'est jamais un domaine ; `Message-ID` n'est pas une adresse e-mail. | Mauvaise qualification. |
| 6 | Contrôler les propositions bancaires. | L'IBAN synthétique commençant par `FR00` est invalide et non intégrable ; aucune attribution pénale n'est déduite du titulaire déclaré. | Promotion automatique ou donnée invalide intégrable. |
| 7 | Fermer avec « Rejeter et fermer ». | Aucun objet persistant nouveau. | Observation, entité ou lien créé. |
| 8 | Relancer, cocher seulement « Conserver dans la fiche » sur quelques propositions. | « Promouvoir en entité » reste décoché par défaut et n'est activable que pour une proposition conservée. | Promotion implicite. |
| 9 | Intégrer la sélection. | Le bilan annonce zéro promotion ; la fiche reste sélectionnée et affiche toutes les observations choisies, leur valeur canonique, type, rôle, source/occurrence, provenance, validation et « Graphe : Non ajoutée ». Aucune ligne `entites` ou `preuve_entites` n'est créée. | Nœud ou rattachement créé par la conservation. |
| 10 | Changer de preuve puis revenir. | Toutes les observations réapparaissent immédiatement. | Observation perdue au rafraîchissement. |
| 11 | Fermer puis rouvrir l'enquête synthétique. | Observations, rôles, origines et état non promu persistent. | Perte de données. |
| 12 | Relancer et promouvoir explicitement une seule observation conservée. | Une seule entité est créée ou réutilisée, un seul nœud apparaît et la fiche indique la promotion. | Plusieurs nœuds ou promotion non demandée. |
| 13 | Répéter la même intégration. | Aucune observation ni entité en double. | Doublon. |
| 14 | Dans « Observations extraites », choisir « Retirer du graphe » et confirmer. | L'observation reste présente avec rôle et provenance ; elle repasse à « Graphe : Non ajoutée ». | Observation supprimée. |
| 15 | Vérifier le graphe. | Le nœud disparaît seulement s'il n'a aucune autre référence. | Suppression d'une entité encore utilisée. |
| 16 | Promouvoir deux rôles vers la même valeur canonique, puis en retirer un. | L'entité et l'autre observation promue restent présentes. | Entité partagée supprimée. |
| 17 | Relier une entité promue à une relation, puis retirer la promotion. | La relation et l'entité restent présentes. | Relation ou entité utilisée supprimée. |
| 18 | Réouvrir l'enquête. | États de conservation, promotion et retrait persistants. | État uniquement visuel. |
| 19 | Créer manuellement le même rattachement preuve-entité qu'une promotion, puis retirer cette promotion. | L'observation est détachée, mais le rattachement manuel, le nœud et l'entité restent présents. | Le rattachement manuel ou l'entité disparaît. |
| 19 | Recalculer l'intégrité. | Le SHA-256 final de la fixture est inchangé. | Empreinte modifiée. |
## Dépendances optionnelles
Répéter l'analyse avec ExifTool, Tesseract ou les outils Poppler absents, selon
les possibilités du poste. L'outil concerné doit être signalé indisponible ou
le résultat partiel ; les en-têtes et l'extraction MIME restent consultables.
Un plantage ou l'impossibilité d'accéder aux résultats EML de base est
bloquant.
## Limites connues
- La promotion est proposée dans le dialogue d'analyse, pas depuis la fiche.
- La fiche affiche encore les codes persistés et ne montre pas séparément la
valeur brute différente ni l'UUID de l'entité.
- Les sorties des outils sont bornées et annulables, mais sans délai maximal
autonome.
- Les fichiers du staging sont nettoyés ; le parcours ne conserve pas encore
les pièces jointes comme dérivés confirmés.
- Tester le retrait avec un rattachement manuel préexistant : V13 conserve sa
source `manual` indépendamment de la source `eml_observation`.

View file

@ -16,7 +16,6 @@ typedef struct BankProposal
char *id; /**< UUID de la proposition */
char *raw_iban; /**< Graphie IBAN brute lue/OCR */
char *normalized_iban; /**< IBAN nettoyé et majuscule */
char *raw_bic; /**< Graphie BIC brute observée */
char *bic; /**< BIC / SWIFT (8 ou 11 car) */
char *holder_name; /**< Titulaire du compte */
char *bank_name; /**< Nom de la banque */
@ -27,7 +26,6 @@ typedef struct BankProposal
char *account_number; /**< Numéro de compte (11 car) */
char *rib_key; /**< Clé RIB (2 ch) */
gboolean is_iban_valid; /**< VRAI si MOD-97 et format valides */
char *iban_validation; /**< Résultat : valid, invalid ou indeterminate */
gboolean is_derived_bban; /**< VRAI si composants dérivés de l'IBAN */
char *suggested_ocr_fix; /**< Proposition de correction OCR (ex: "O->0") */
char *verification_status; /**< Code contrôlé: proposed, confirmed, rejected, etc. */

View file

@ -1,73 +0,0 @@
/******************************************************************************
* @file document_analysis.h
* @brief Modèles communs pour les analyses documentaires en mémoire.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_DOCUMENT_ANALYSIS_H
#define LABFY_INVESTIGATION_DOCUMENT_ANALYSIS_H
#include <gio/gio.h>
G_BEGIN_DECLS
#define DOCUMENT_ANALYSIS_MAX_FILE_SIZE (50U * 1024U * 1024U)
#define DOCUMENT_ANALYSIS_MAX_STDOUT (8U * 1024U * 1024U)
#define DOCUMENT_ANALYSIS_MAX_STDERR (256U * 1024U)
#define DOCUMENT_ANALYSIS_MAX_TEXT (8U * 1024U * 1024U)
#define DOCUMENT_ANALYSIS_MAX_PDF_PAGES 100U
#define DOCUMENT_ANALYSIS_MAX_PIPELINE_ITEMS 128U
typedef enum
{
DOCUMENT_ANALYSIS_STATE_SUCCESS,
DOCUMENT_ANALYSIS_STATE_PARTIAL,
DOCUMENT_ANALYSIS_STATE_UNAVAILABLE,
DOCUMENT_ANALYSIS_STATE_CANCELLED,
DOCUMENT_ANALYSIS_STATE_FAILED,
DOCUMENT_ANALYSIS_STATE_INCOMPATIBLE
} DocumentAnalysisState;
typedef struct
{
char *tool_id;
char *version;
GPtrArray *arguments;
char *started_at_utc;
char *finished_at_utc;
char *source_path;
char *source_sha256;
char *raw_stdout;
char *raw_stdout_sha256;
char *raw_stderr;
gsize stdout_bytes_observed;
gsize stderr_bytes_observed;
gboolean stdout_truncated;
gboolean stderr_truncated;
int exit_status;
DocumentAnalysisState state;
GPtrArray *warnings;
GPtrArray *errors;
} DocumentToolExecution;
typedef struct
{
char *code;
char *original_group;
char *original_tag;
char *raw_value;
gboolean sensitive;
gboolean requires_confirmation;
} DocumentMetadataEntry;
DocumentToolExecution *document_tool_execution_new(
const char *tool_id,
const char *source_path
);
void document_tool_execution_free(DocumentToolExecution *execution);
void document_tool_execution_add_argument(
DocumentToolExecution *execution,
const char *argument
);
const char *document_analysis_state_code(DocumentAnalysisState state);
G_END_DECLS
#endif

View file

@ -1,48 +0,0 @@
/******************************************************************************
* @file document_file_analysis.h
* @brief Orchestration des analyses compatibles d'un fichier dérivé.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_DOCUMENT_FILE_ANALYSIS_H
#define LABFY_INVESTIGATION_DOCUMENT_FILE_ANALYSIS_H
#include "core/exiftool_analysis.h"
#include "core/ocr_analysis.h"
#include "core/pdf_analysis.h"
G_BEGIN_DECLS
typedef struct
{
const char *exiftool;
const char *tesseract;
const char *pdfinfo;
const char *pdftotext;
const char *pdftoppm;
} DocumentAnalysisTools;
typedef struct
{
char *source_path;
char *declared_mime;
char *detected_mime;
ExiftoolAnalysisResult *metadata;
OcrAnalysisResult *ocr;
PdfAnalysisResult *pdf;
DocumentAnalysisState state;
GPtrArray *warnings;
} DocumentFileAnalysis;
DocumentFileAnalysis *document_file_analysis_run(
const DocumentAnalysisTools *tools,
const char *source_path,
const char *declared_mime,
const char *detected_mime,
gboolean request_image_ocr,
const char *ocr_languages,
GCancellable *cancellable,
GError **error
);
void document_file_analysis_free(DocumentFileAnalysis *analysis);
G_END_DECLS
#endif

View file

@ -1,46 +0,0 @@
/******************************************************************************
* @file document_tool_runner.h
* @brief Exécution bornée et annulable des outils documentaires.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_DOCUMENT_TOOL_RUNNER_H
#define LABFY_INVESTIGATION_DOCUMENT_TOOL_RUNNER_H
#include "core/document_analysis.h"
G_BEGIN_DECLS
typedef struct
{
gsize stdout_limit;
gsize stderr_limit;
} DocumentToolRunnerLimits;
gboolean document_tool_runner_run(
const char *tool_id,
const char *executable,
const char *const arguments[],
const char *source_path,
GCancellable *cancellable,
DocumentToolExecution **out_execution,
GError **error
);
gboolean document_tool_runner_run_with_limits(
const char *tool_id,
const char *executable,
const char *const arguments[],
const char *source_path,
const DocumentToolRunnerLimits *limits,
GCancellable *cancellable,
DocumentToolExecution **out_execution,
GError **error
);
char *document_tool_runner_read_version(
const char *executable,
const char *const arguments[],
GCancellable *cancellable
);
G_END_DECLS
#endif

View file

@ -8,15 +8,6 @@
G_BEGIN_DECLS
/** @brief Résultat opaque d'une analyse d'en-têtes EML. */
typedef struct EmlAnalysis EmlAnalysis;
typedef struct {
char *type_identifier;
char *value_raw;
char *value_normalized;
char *role;
char *source_header;
guint occurrence;
char *provenance_kind;
} EmlObservation;
/**
* @brief Analyse uniquement les en-têtes d'un fichier EML local.
* @param file_path Chemin du fichier original en lecture seule.
@ -36,10 +27,6 @@ const GPtrArray *eml_analysis_get_header_values(const EmlAnalysis *analysis,
const GPtrArray *eml_analysis_get_email_addresses(const EmlAnalysis *analysis);
/** @brief Retourne les domaines uniques extraits. */
const GPtrArray *eml_analysis_get_domains(const EmlAnalysis *analysis);
/** @brief Retourne les observations sémantiques ordonnées. */
const GPtrArray *eml_analysis_get_observations(const EmlAnalysis *analysis);
/** @brief Libère une observation autonome. */
void eml_observation_free(EmlObservation *observation);
/** @brief Retourne les adresses IP uniques extraites. */
const GPtrArray *eml_analysis_get_ip_addresses(const EmlAnalysis *analysis);
/** @brief Retourne les IP présentes dans la partie `from` des Received. */
@ -50,11 +37,5 @@ const GPtrArray *eml_analysis_get_destination_ip_addresses(
const EmlAnalysis *analysis);
/** @brief Retourne une copie UTF-8 des en-têtes bruts. */
const char *eml_analysis_get_raw_headers(const EmlAnalysis *analysis);
/**
* @brief Retourne la date du message normalisée en UTC, ou NULL.
*
* La valeur brute reste disponible via l'en-tête `Date`.
*/
const char *eml_analysis_get_date_utc(const EmlAnalysis *analysis);
G_END_DECLS
#endif

View file

@ -13,11 +13,6 @@ typedef struct {
char *value;
char *verification_status;
char *provenance_kind;
char *value_raw;
char *role;
char *source_header;
guint occurrence;
gboolean promote_to_entity;
} EmlEntityProposal;
/** @brief Crée une proposition possédée. */
EmlEntityProposal *eml_entity_proposal_new(const char *type_identifier,
@ -25,11 +20,6 @@ EmlEntityProposal *eml_entity_proposal_new(const char *type_identifier,
EmlEntityProposal *eml_entity_proposal_new_with_metadata(
const char *type_identifier, const char *value,
const char *verification_status, const char *provenance_kind);
EmlEntityProposal *eml_entity_proposal_new_observation(
const char *type_identifier, const char *value_raw,
const char *value_normalized, const char *role,
const char *source_header, guint occurrence,
const char *verification_status, const char *provenance_kind);
/** @brief Libère une proposition. */
void eml_entity_proposal_free(EmlEntityProposal *proposal);
/**
@ -44,12 +34,6 @@ void eml_entity_proposal_free(EmlEntityProposal *proposal);
*/
gboolean eml_integration_apply(Database *database,
const char *evidence_identifier, const GPtrArray *proposals,
guint *out_observations, guint *out_created, guint *out_reused,
GError **error);
/** @brief Annule une promotion sans supprimer l'observation. */
gboolean eml_integration_remove_promotion(Database *database,
const char *observation_identifier, gboolean *out_entity_deleted,
gboolean *out_entity_shared, GError **error);
guint *out_created, guint *out_reused, GError **error);
G_END_DECLS
#endif

View file

@ -5,39 +5,23 @@
#ifndef LABFY_INVESTIGATION_EML_MIME_EXTRACTOR_H
#define LABFY_INVESTIGATION_EML_MIME_EXTRACTOR_H
#include <gio/gio.h>
#include <glib.h>
G_BEGIN_DECLS
/** @brief Limites de sécurité centralisées de l'extracteur MIME. */
#define EML_MIME_MAX_FILE_SIZE (50U * 1024U * 1024U)
#define EML_MIME_MAX_PART_DECODED_SIZE (8U * 1024U * 1024U)
#define EML_MIME_MAX_TOTAL_DECODED_SIZE (32U * 1024U * 1024U)
#define EML_MIME_MAX_PARTS 128U
#define EML_MIME_MAX_DEPTH 12U
#define EML_MIME_MAX_FILENAME_LENGTH 240U
#define EML_MIME_MAX_HEADER_VALUE_LENGTH (64U * 1024U)
/** @brief Représentation d'une pièce jointe extraite d'un message EML. */
typedef struct EmlAttachment
{
char *part_index; /**< Chemin/index MIME (ex: "1.2") */
char *declared_filename; /**< Nom de fichier d'origine */
char *decoded_filename; /**< Nom déclaré décodé RFC 2047/2231 */
char *sanitized_filename; /**< Nom assaini (anti path-traversal) */
char *extracted_path; /**< Chemin absolu dans 02_Preuves_Traitees */
char *relative_path; /**< Chemin relatif par rapport à la racine d'enquête */
char *content_type; /**< Type MIME déclaré */
char *detected_mime; /**< Type MIME détecté */
char *content_id; /**< Content-ID pour les images/pièces inline */
char *normalized_content_id;/**< Content-ID sans chevrons */
char *content_disposition; /**< Content-Disposition brut de la partie */
char *normalized_disposition; /**< attachment, inline ou NULL */
char *transfer_encoding; /**< Content-Transfer-Encoding */
char *extracted_at_utc; /**< Date UTC de l'extraction */
gboolean is_inline; /**< VRAI si disposition inline */
gboolean is_attachment; /**< VRAI si disposition attachment */
gboolean is_truncated; /**< VRAI si le contenu a dû être tronqué */
gsize encoded_size; /**< Taille encodée */
gsize decoded_size; /**< Taille décodée */
char *sha256; /**< Empreinte SHA-256 du fichier extrait */
@ -79,22 +63,6 @@ EmlMimeResult *eml_mime_extract_attachments(const char *eml_path,
const char *target_dir,
GError **error);
/**
* @brief Variante annulable de l'extraction MIME.
*
* @param eml_path Chemin du fichier EML source.
* @param target_dir Dossier de destination.
* @param cancellable Objet d'annulation facultatif.
* @param error Destination d'erreur facultative.
* @return Résultat MIME, ou NULL en cas d'erreur.
*/
EmlMimeResult *eml_mime_extract_attachments_cancellable(
const char *eml_path,
const char *target_dir,
GCancellable *cancellable,
GError **error
);
G_END_DECLS
#endif /* LABFY_INVESTIGATION_EML_MIME_EXTRACTOR_H */

View file

@ -9,7 +9,6 @@
#include "core/bank_proposal.h"
#include "core/eml_analyzer.h"
#include "core/eml_mime_extractor.h"
#include "core/document_file_analysis.h"
G_BEGIN_DECLS
@ -19,10 +18,7 @@ typedef struct EmlPipelineResult
EmlAnalysis *analysis; /**< Analyse des en-têtes EML */
EmlMimeResult *mime_result; /**< Pièces jointes extraites */
GPtrArray *bank_proposals; /**< Tableau de BankProposal* */
GPtrArray *document_analyses; /**< Tableau de DocumentFileAnalysis* */
GPtrArray *warnings; /**< Avertissements globaux */
DocumentAnalysisState state;
guint skipped_document_analyses;
} EmlPipelineResult;
/** @brief Libère un résultat EmlPipelineResult. */
@ -39,31 +35,6 @@ BackgroundTask *eml_pipeline_task_new(const char *eml_path,
const char *processed_evidence_dir,
const char *evidence_id);
BackgroundTask *eml_pipeline_task_new_with_tools(
const char *eml_path,
const char *processed_evidence_dir,
const char *evidence_id,
const DocumentAnalysisTools *tools
);
BackgroundTask *eml_pipeline_task_new_with_tools_and_limit(
const char *eml_path,
const char *processed_evidence_dir,
const char *evidence_id,
const DocumentAnalysisTools *tools,
guint document_analysis_limit
);
BackgroundTask *eml_pipeline_task_start(
const char *eml_path,
const char *staging_directory,
const char *evidence_id,
const DocumentAnalysisTools *tools,
BackgroundTaskCompletionCallback completion_callback,
gpointer completion_data,
GDestroyNotify completion_data_destroy
);
G_END_DECLS
#endif /* LABFY_INVESTIGATION_EML_PIPELINE_TASK_H */

View file

@ -1,42 +0,0 @@
/******************************************************************************
* @file exiftool_analysis.h
* @brief Analyse ExifTool structurée et traçable.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_EXIFTOOL_ANALYSIS_H
#define LABFY_INVESTIGATION_EXIFTOOL_ANALYSIS_H
#include "core/document_analysis.h"
#include "core/document_tool_runner.h"
G_BEGIN_DECLS
typedef struct
{
DocumentToolExecution *execution;
GPtrArray *metadata;
} ExiftoolAnalysisResult;
ExiftoolAnalysisResult *exiftool_analysis_run(
const char *executable,
const char *file_path,
GCancellable *cancellable,
GError **error
);
ExiftoolAnalysisResult *exiftool_analysis_run_with_limits(
const char *executable,
const char *file_path,
const DocumentToolRunnerLimits *limits,
GCancellable *cancellable,
GError **error
);
ExiftoolAnalysisResult *exiftool_analysis_parse(
const char *file_path,
const char *json,
const char *stderr_text,
int exit_status,
GError **error
);
void exiftool_analysis_result_free(ExiftoolAnalysisResult *result);
G_END_DECLS
#endif

View file

@ -1,39 +0,0 @@
/******************************************************************************
* @file ocr_analysis.h
* @brief OCR Tesseract traçable et annulable.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_OCR_ANALYSIS_H
#define LABFY_INVESTIGATION_OCR_ANALYSIS_H
#include "core/document_analysis.h"
#include "core/document_tool_runner.h"
G_BEGIN_DECLS
typedef struct
{
DocumentToolExecution *execution;
char *requested_languages;
char *text;
} OcrAnalysisResult;
OcrAnalysisResult *ocr_analysis_run(
const char *executable,
const char *image_path,
const char *languages,
GCancellable *cancellable,
GError **error
);
OcrAnalysisResult *ocr_analysis_run_with_limits(
const char *executable,
const char *image_path,
const char *languages,
const DocumentToolRunnerLimits *limits,
GCancellable *cancellable,
GError **error
);
void ocr_analysis_result_free(OcrAnalysisResult *result);
gboolean ocr_analysis_mime_is_compatible(const char *mime_type);
G_END_DECLS
#endif

View file

@ -1,63 +0,0 @@
/******************************************************************************
* @file pdf_analysis.h
* @brief Extraction PDF native puis OCR de secours.
******************************************************************************/
#ifndef LABFY_INVESTIGATION_PDF_ANALYSIS_H
#define LABFY_INVESTIGATION_PDF_ANALYSIS_H
#include "core/document_analysis.h"
#include "core/ocr_analysis.h"
G_BEGIN_DECLS
typedef enum
{
PDF_PAGE_METHOD_NATIVE,
PDF_PAGE_METHOD_OCR
} PdfPageMethod;
typedef struct
{
guint page_number;
PdfPageMethod method;
char *text;
DocumentAnalysisState state;
DocumentToolExecution *render_execution;
DocumentToolExecution *execution;
GPtrArray *warnings;
} PdfPageAnalysis;
typedef struct
{
const char *pdfinfo;
const char *pdftotext;
const char *pdftoppm;
const char *tesseract;
} PdfAnalysisTools;
typedef struct
{
char *source_path;
gboolean encrypted;
guint page_count;
char *native_text;
gboolean native_text_usable;
DocumentAnalysisState state;
DocumentToolExecution *pdfinfo_execution;
DocumentToolExecution *native_execution;
GPtrArray *pages;
GPtrArray *warnings;
} PdfAnalysisResult;
gboolean pdf_analysis_text_is_usable(const char *text);
PdfAnalysisResult *pdf_analysis_run(
const PdfAnalysisTools *tools,
const char *pdf_path,
const char *ocr_languages,
GCancellable *cancellable,
GError **error
);
void pdf_analysis_result_free(PdfAnalysisResult *result);
G_END_DECLS
#endif

View file

@ -7,7 +7,6 @@
#define LABFY_INVESTIGATION_EVIDENCE_ENTITY_DAO_H
#include "database/database.h"
#include "models/evidence_observation.h"
#include <glib.h>
@ -151,16 +150,6 @@ gboolean evidence_entity_dao_unlink(
GError **error
);
gboolean evidence_entity_dao_add_source(
EvidenceEntityDao *dao, const char *evidence_identifier,
const char *entity_identifier, const char *source_kind,
const char *source_uuid, const char *created_at, GError **error);
gboolean evidence_entity_dao_remove_source(
EvidenceEntityDao *dao, const char *evidence_identifier,
const char *entity_identifier, const char *source_kind,
const char *source_uuid, gboolean *out_link_removed, GError **error);
/**
* @brief Vérifie si une association existe.
*
@ -224,27 +213,6 @@ GPtrArray *evidence_entity_dao_list_evidence_identifiers(
GError **error
);
gboolean evidence_entity_dao_add_observation(
EvidenceEntityDao *dao, const char *evidence_identifier,
const char *entity_type,
const char *value_raw, const char *value_normalized,
const char *role, const char *provenance_kind,
const char *source_header, guint occurrence,
const char *verification_status, const char *created_at,
char **out_observation_identifier, GError **error);
gboolean evidence_entity_dao_promote_observation(
EvidenceEntityDao *dao, const char *observation_identifier,
const char *entity_identifier, const char *promoted_at,
const char *promotion_kind, GError **error);
/** @brief Construit le texte persistant affiché dans la fiche preuve. */
char *evidence_entity_dao_format_observations(
EvidenceEntityDao *dao, const char *evidence_identifier,
GError **error);
GPtrArray *evidence_entity_dao_list_observations(
EvidenceEntityDao *dao, const char *evidence_identifier, GError **error);
G_END_DECLS
#endif

View file

@ -91,9 +91,6 @@ bool schema_install_v7(Database *database);
bool schema_install_v8(Database *database);
bool schema_install_v9(Database *database);
bool schema_install_v10(Database *database);
bool schema_install_v11(Database *database);
bool schema_install_v12(Database *database);
bool schema_install_v13(Database *database);
/**
* @brief Garantit la présence des extensions du schéma courant V2.

View file

@ -1,18 +0,0 @@
#ifndef LABFY_INVESTIGATION_EVIDENCE_OBSERVATION_H
#define LABFY_INVESTIGATION_EVIDENCE_OBSERVATION_H
#include <glib.h>
typedef struct {
char *identifier;
char *value;
char *type_identifier;
char *role;
char *source_header;
guint occurrence;
char *provenance_kind;
char *verification_status;
char *integrated_at;
char *entity_identifier;
char *promotion_kind;
} EvidenceObservation;
void evidence_observation_free(EvidenceObservation *observation);
#endif

View file

@ -6,7 +6,6 @@
#define LABFY_INVESTIGATION_EML_ANALYSIS_DIALOG_H
#include "core/eml_processing.h"
#include "core/eml_integration.h"
#include "core/eml_pipeline_task.h"
#include <gtk/gtk.h>
G_BEGIN_DECLS
/** @brief Callback recevant les propositions sélectionnées, ou NULL. */
@ -20,14 +19,5 @@ typedef void (*EmlAnalysisDialogCallback)(GPtrArray *proposals,
void eml_analysis_dialog_present(GtkWindow *parent,
const EmlProcessingResult *result, EmlAnalysisDialogCallback callback,
gpointer user_data);
void eml_analysis_dialog_present_pipeline(
GtkWindow *parent,
const EmlPipelineResult *result,
const char *evidence_name,
const char *relative_path,
const char *source_sha256,
EmlAnalysisDialogCallback callback,
gpointer user_data
);
G_END_DECLS
#endif

View file

@ -497,14 +497,6 @@ void main_window_set_selected_evidence(
MainWindow *main_window,
const EvidenceRecord *evidence_record
);
void main_window_set_evidence_observations(MainWindow *main_window,
const GPtrArray *observations);
void main_window_set_observation_remove_callback(MainWindow *main_window,
void (*callback)(const char *, gpointer), gpointer user_data);
void main_window_set_eml_analysis_available(
MainWindow *main_window,
gboolean available
);
/** @brief Transmet au workspace un chemin absolu validé pour l'aperçu. */
void main_window_set_evidence_preview(MainWindow *main_window,

View file

@ -8,7 +8,6 @@
#include "core/investigation_node.h"
#include "models/evidence_record.h"
#include "models/evidence_observation.h"
#include "models/entity_record.h"
#include "models/osint_action_catalog.h"
@ -154,8 +153,6 @@ typedef void (*WorkspaceExtractMetadataCallback)(const char *evidence_identifier
/** @brief Callback appelé pour récupérer le mot de passe d'un PDF. */
typedef void (*WorkspaceRecoverPdfPasswordCallback)(
const char *evidence_identifier, gpointer user_data);
typedef void (*WorkspaceObservationRemoveCallback)(
const char *observation_identifier, gpointer user_data);
/**
* @brief Crée une nouvelle zone de travail.
@ -206,14 +203,6 @@ void workspace_set_selected_evidence(
Workspace *workspace,
const EvidenceRecord *evidence_record
);
void workspace_set_evidence_observations(Workspace *workspace,
const GPtrArray *observations);
void workspace_set_observation_remove_callback(Workspace *workspace,
WorkspaceObservationRemoveCallback callback, gpointer user_data);
void workspace_set_eml_analysis_available(
Workspace *workspace,
gboolean available
);
/**
* @brief Affiche l'aperçu local d'un fichier image ou vidéo.

View file

@ -1,151 +0,0 @@
{
"$schema": "https://opencode.ai/config.json",
"enabled_providers": [
"llama.cpp"
],
"provider": {
"llama.cpp": {
"npm": "@ai-sdk/openai-compatible",
"name": "llama-server local",
"options": {
"baseURL": "http://127.0.0.1:8080/v1",
"timeout": 600000,
"chunkTimeout": 180000
},
"models": {
"qwen3-8b": {
"name": "Qwen3 8B — Architecte et reviewer",
"limit": {
"context": 32768,
"output": 2048
}
},
"qwen2.5-coder-7b": {
"name": "Qwen2.5-Coder 7B — Développeur C17",
"limit": {
"context": 32768,
"output": 2048
}
}
}
}
},
"model": "llama.cpp/qwen3-8b",
"small_model": "llama.cpp/qwen3-8b",
"default_agent": "orchestrateur",
"subagent_depth": 1,
"instructions": [
".opencode/labfy.md"
],
"compaction": {
"auto": true,
"prune": true,
"reserved": 8000
},
"permission": {
"external_directory": "deny",
"edit": "ask",
"bash": "ask"
},
"agent": {
"orchestrateur": {
"description": "Agent principal. Inspecte le dépôt, prépare le plan, consulte larchitecte, délègue la production de code C17, applique les changements autorisés, compile, teste et présente le résultat.",
"mode": "primary",
"model": "llama.cpp/qwen3-8b",
"temperature": 0.1,
"steps": 40,
"permission": {
"read": "allow",
"glob": "allow",
"grep": "allow",
"list": "allow",
"edit": "ask",
"bash": {
"*": "ask",
"pwd": "allow",
"ls *": "allow",
"grep *": "allow",
"rg *": "allow",
"head *": "allow",
"tail *": "allow",
"file *": "allow",
"git status*": "allow",
"git diff*": "allow",
"git log*": "allow",
"make -j8*": "allow",
"make test*": "allow",
"git commit*": "deny",
"git push*": "deny",
"git reset*": "deny",
"git clean*": "deny",
"git checkout*": "deny",
"git restore*": "deny",
"rm *": "deny",
"sudo *": "deny",
"doas *": "deny"
},
"task": {
"*": "deny",
"architecte-reviewer": "allow",
"coder-c17": "allow"
},
"external_directory": "deny",
"todowrite": "allow",
"lsp": "allow",
"question": "allow",
"skill": "deny",
"websearch": "allow",
"webfetch": "allow"
},
"prompt": "Tu pilotes le travail dans Labfy Investigation. Pour une nouvelle API, une migration, une modification transversale ou un changement sensible, consulte architecte-reviewer avant limplémentation. Transmets ensuite à coder-c17 tout le contexte nécessaire : objectif, fichiers pertinents, interfaces, extraits de code, contraintes et diagnostics. Le Coder ne dispose daucun outil : il renvoie uniquement du code ou un patch textuel. Tu restes seul responsable de lapplication des changements, de lexécution de make -j8, des tests, de git diff --check et de la validation finale. Ne crée jamais de commit ni de push sans autorisation explicite."
},
"coder-c17": {
"description": "Spécialiste ISO C17 sans outils. Reçoit le contexte transmis par lorchestrateur et produit du code ou un patch textuel précis.",
"mode": "all",
"model": "llama.cpp/qwen2.5-coder-7b",
"temperature": 0.1,
"permission": {
"read": "deny",
"glob": "deny",
"grep": "deny",
"list": "deny",
"edit": "deny",
"bash": "deny",
"task": "deny",
"external_directory": "deny",
"todowrite": "deny",
"lsp": "deny",
"skill": "deny",
"question": "deny",
"webfetch": "deny",
"websearch": "deny",
"doom_loop": "deny"
},
"prompt": "Tu es le spécialiste C17 de Labfy Investigation. Tu ne disposes daucun outil et tu ne peux ni lire le dépôt, ni modifier un fichier, ni compiler, ni exécuter un test. Travaille uniquement à partir du contexte transmis. Produis du code ISO C17 compatible avec -std=c17 -Wall -Wextra -Wpedantic -Werror. Respecte les interfaces existantes, les noms snake_case, les préfixes de modules, lownership GLib, larchitecture GTK4/DAO/services et SQLite. Retourne de préférence un diff unifié applicable, ou des blocs de code complets avec les chemins concernés. Signale clairement les hypothèses. Ne prétends jamais avoir lu, modifié, compilé ou testé quoi que ce soit."
},
"architecte-reviewer": {
"description": "Architecte et reviewer en lecture seule. Contrôle larchitecture, la cohérence des API, lownership, GTK4/GLib, SQLite, les risques de régression et les tests manquants.",
"mode": "subagent",
"model": "llama.cpp/qwen3-8b",
"temperature": 0.1,
"steps": 30,
"permission": {
"read": "allow",
"glob": "allow",
"grep": "allow",
"list": "allow",
"edit": "deny",
"bash": "deny",
"task": "deny",
"external_directory": "deny",
"todowrite": "deny",
"lsp": "allow",
"skill": "deny",
"question": "deny",
"webfetch": "deny",
"websearch": "deny"
},
"prompt": "Tu es larchitecte et reviewer de Labfy Investigation. Tu travailles strictement en lecture seule. Examine le plan, les fichiers pertinents et le diff. Vérifie la séparation widgets/modèles/DAO/services, la cohérence entre .h et .c, lownership et les durées de vie GLib/GTK, les transactions SQLite, la gestion derreurs, la sécurité, les régressions et les tests manquants. Classe les remarques par gravité : bloquant, important, amélioration. Propose des corrections précises sans modifier le dépôt et sans créer de commit."
}
}
}

View file

@ -58,8 +58,6 @@
#include "core/person_entity_service.h"
#include "core/eml_processing.h"
#include "core/eml_integration.h"
#include "core/eml_pipeline_task.h"
#include "core/file_hash.h"
#include "database/database.h"
#include "database/transaction.h"
#include "views/create_social_account_dialog.h"
@ -150,22 +148,6 @@ struct Application
char *pending_entity_selection_identifier;
};
static void application_on_evidence_selected(
const char *evidence_identifier, gpointer user_data);
typedef struct {
Application *application;
char *observation_identifier;
} ApplicationObservationRemovalContext;
static void application_observation_removal_context_free(
ApplicationObservationRemovalContext *context)
{
if (context == NULL) return;
g_free(context->observation_identifier);
g_free(context);
}
/**
* @brief Contexte ApplicationOpenErrorconservé jusquà la fin dun import.
*/
@ -267,15 +249,7 @@ typedef struct
static char *application_osint_output_to_utf8(GBytes *bytes);
static char *application_osint_create_timestamp(void);
/** @brief Contexte possédé pendant la révision d'une analyse EML. */
typedef struct {
Application *application;
InvestigationSession *expected_session;
char *evidence_identifier;
char *evidence_name;
char *relative_path;
char *source_sha256;
char *staging_directory;
}
typedef struct { Application *application; char *evidence_identifier; }
ApplicationEmlReviewContext;
static void application_start_graph_loading(
@ -3764,48 +3738,7 @@ cleanup:
/** @brief Libère le contexte de révision EML. */
static void application_eml_review_context_free(ApplicationEmlReviewContext *context)
{
if (context == NULL) return;
g_free(context->evidence_identifier);
g_free(context->evidence_name);
g_free(context->relative_path);
g_free(context->source_sha256);
g_free(context->staging_directory);
g_free(context);
}
/** @brief Supprime récursivement une zone de staging créée par l'application. */
static void application_remove_staging_directory(const char *path)
{
if (path == NULL) return;
GFile *directory = g_file_new_for_path(path);
GError *error = NULL;
GFileEnumerator *enumerator = g_file_enumerate_children(directory,
G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE,
G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, &error);
if (enumerator != NULL)
{
GFileInfo *info = NULL;
while ((info = g_file_enumerator_next_file(
enumerator, NULL, &error)) != NULL)
{
GFile *child = g_file_get_child(directory,
g_file_info_get_name(info));
char *child_path = g_file_get_path(child);
if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY)
application_remove_staging_directory(child_path);
else
(void) g_file_delete(child, NULL, NULL);
g_free(child_path);
g_object_unref(child);
g_object_unref(info);
}
g_object_unref(enumerator);
}
g_clear_error(&error);
(void) g_file_delete(directory, NULL, NULL);
g_object_unref(directory);
}
{ if (context == NULL) return; g_free(context->evidence_identifier); g_free(context); }
/** @brief Intègre la sélection EML puis actualise le graphe. */
static void application_on_eml_selection_completed(
@ -3814,96 +3747,29 @@ static void application_on_eml_selection_completed(
ApplicationEmlReviewContext *context = user_data;
Application *application = context != NULL ? context->application : NULL;
const InvestigationProject *project = NULL;
GError *error = NULL; guint observations = 0, created = 0, reused = 0;
char *message = NULL;
GError *error = NULL; guint created = 0, reused = 0; char *message = NULL;
if (proposals == NULL)
{
application_remove_staging_directory(context != NULL
? context->staging_directory : NULL);
application_eml_review_context_free(context);
return;
}
{ application_eml_review_context_free(context); return; }
if (application == NULL || application->session == NULL ||
!eml_integration_apply(investigation_session_get_database(application->session),
context->evidence_identifier, proposals, &observations,
&created, &reused, &error))
context->evidence_identifier, proposals, &created, &reused, &error))
application_present_error(application, "Intégration EML impossible",
error != NULL ? error->message : "La transaction a échoué.");
else
{
message = g_strdup_printf(
"%u observation(s) conservée(s), %u entité(s) créée(s), "
"%u réutilisée(s).", observations, created, reused);
message = g_strdup_printf("%u entité(s) créée(s), %u réutilisée(s).",
created, reused);
application_message_dialog_present(main_window_get_window(application->main_window),
APPLICATION_MESSAGE_DIALOG_INFORMATION, "Analyse EML intégrée", message);
if (created + reused > 0)
{
project = investigation_session_get_project(application->session);
application_start_graph_loading(application,
investigation_project_get_database_path(project));
}
(void) application_refresh_evidence_models(application, NULL);
application_on_evidence_selected(context->evidence_identifier,
application);
}
g_free(message); g_clear_error(&error); g_ptr_array_unref(proposals);
application_remove_staging_directory(context != NULL
? context->staging_directory : NULL);
application_eml_review_context_free(context);
}
/** @brief Termine le pipeline EML sur le contexte GTK principal. */
static void application_on_eml_pipeline_completed(
BackgroundTask *task,
gpointer user_data)
{
ApplicationEmlReviewContext *context = user_data;
Application *application = context != NULL ? context->application : NULL;
BackgroundTaskState state = background_task_get_state(task);
if (application == NULL || application->main_window == NULL ||
application->session == NULL ||
application->session != context->expected_session)
{
application_remove_staging_directory(context != NULL
? context->staging_directory : NULL);
application_eml_review_context_free(context);
return;
}
if (state == BACKGROUND_TASK_STATE_CANCELLED)
{
main_window_set_status(application->main_window,
"Analyse de le-mail annulée.");
application_remove_staging_directory(context->staging_directory);
application_eml_review_context_free(context);
return;
}
if (state != BACKGROUND_TASK_STATE_COMPLETED)
{
GError *error = background_task_dup_error(task);
application_present_error(application, "Analyse EML impossible",
error != NULL ? error->message : "Le pipeline EML a échoué.");
g_clear_error(&error);
application_remove_staging_directory(context->staging_directory);
application_eml_review_context_free(context);
return;
}
EmlPipelineResult *result = background_task_get_result(task);
if (result == NULL)
{
application_present_error(application, "Analyse EML impossible",
"Le pipeline na produit aucun résultat.");
application_remove_staging_directory(context->staging_directory);
application_eml_review_context_free(context);
return;
}
eml_analysis_dialog_present_pipeline(
main_window_get_window(application->main_window),
result, context->evidence_name, context->relative_path,
context->source_sha256, application_on_eml_selection_completed,
context);
}
/** @brief Vérifie la preuve puis lance le pipeline EML en arrière-plan. */
/** @brief Prépare une copie vérifiée puis affiche l'analyse EML locale. */
static void application_on_analyze_eml_requested(
const char *evidence_identifier, gpointer user_data)
{
@ -3911,14 +3777,8 @@ static void application_on_analyze_eml_requested(
const InvestigationProject *project = NULL;
EvidenceDao *dao = NULL;
EvidenceRecord *record = NULL;
EmlProcessingResult *result = NULL;
ApplicationEmlReviewContext *context = NULL;
BackgroundTask *task = NULL;
char *root = NULL;
char *candidate = NULL;
char *source_path = NULL;
char *current_sha256 = NULL;
guint64 current_size = 0;
char *staging = NULL;
GError *error = NULL;
if (application == NULL || application->session == NULL ||
evidence_identifier == NULL) return;
@ -3927,93 +3787,22 @@ static void application_on_analyze_eml_requested(
if (dao != NULL) record = evidence_dao_find_by_identifier(
dao, evidence_identifier, &error);
project = investigation_session_get_project(application->session);
if (record == NULL || project == NULL) goto failure;
root = g_canonicalize_filename(
investigation_project_get_root_path(project), NULL);
candidate = g_build_filename(root,
evidence_record_get_relative_path(record), NULL);
source_path = g_canonicalize_filename(candidate, NULL);
const char *name = evidence_record_get_original_name(record);
char *lower = name != NULL ? g_ascii_strdown(name, -1) : NULL;
gboolean is_eml = lower != NULL && g_str_has_suffix(lower, ".eml");
g_free(lower);
if (!is_eml || source_path == NULL ||
!g_str_has_prefix(source_path, root) ||
(source_path[strlen(root)] != G_DIR_SEPARATOR &&
source_path[strlen(root)] != '\0') ||
!g_file_test(source_path, G_FILE_TEST_IS_REGULAR))
if (record != NULL && project != NULL)
result = eml_processing_prepare(
investigation_project_get_root_path(project), record, &error);
if (result == NULL)
application_present_error(application, "Analyse EML impossible",
error != NULL ? error->message : "La copie de travail n'a pas pu être analysée.");
else
{
g_set_error_literal(&error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
"La preuve sélectionnée nest pas un fichier EML exploitable.");
goto failure;
}
if (!file_hash_compute_sha256(source_path, NULL, &current_sha256,
&current_size, &error) ||
g_strcmp0(current_sha256, evidence_record_get_sha256(record)) != 0)
{
if (error == NULL)
g_set_error_literal(&error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
"Lempreinte SHA-256 de la preuve diverge de "
"lempreinte enregistrée. Lanalyse est bloquée.");
goto failure;
}
staging = g_dir_make_tmp("labfy-eml-staging-XXXXXX", &error);
if (staging == NULL) goto failure;
context = g_new0(ApplicationEmlReviewContext, 1);
context->application = application;
context->expected_session = application->session;
context->evidence_identifier = g_strdup(evidence_identifier);
context->evidence_name = g_strdup(name);
context->relative_path = g_strdup(
evidence_record_get_relative_path(record));
context->source_sha256 = g_strdup(current_sha256);
context->staging_directory = staging;
staging = NULL;
DocumentAnalysisTools tools = {
.exiftool = "exiftool", .tesseract = "tesseract",
.pdfinfo = "pdfinfo", .pdftotext = "pdftotext",
.pdftoppm = "pdftoppm"
};
task = eml_pipeline_task_start(source_path, context->staging_directory,
evidence_identifier, &tools, application_on_eml_pipeline_completed,
context, NULL);
if (task == NULL)
goto failure;
context = NULL;
if (!task_manager_add(application->task_manager, task, &error))
{
background_task_cancel(task);
goto cleanup;
eml_analysis_dialog_present(main_window_get_window(application->main_window),
result, application_on_eml_selection_completed, context);
}
main_window_set_status(application->main_window,
"Analyse de le-mail lancée en arrière-plan.");
background_task_unref(task);
task = NULL;
goto cleanup;
failure:
if (task != NULL)
background_task_cancel(task);
if (context != NULL)
application_remove_staging_directory(context->staging_directory);
else
application_remove_staging_directory(staging);
if (context != NULL)
application_eml_review_context_free(context);
if (application != NULL)
application_present_error(application, "Analyse EML impossible",
error != NULL ? error->message :
"La preuve na pas pu être analysée.");
cleanup:
background_task_unref(task);
g_free(staging);
g_free(current_sha256);
g_free(source_path);
g_free(candidate);
g_free(root);
evidence_record_free(record);
evidence_dao_free(dao);
g_clear_error(&error);
eml_processing_result_free(result); evidence_record_free(record);
evidence_dao_free(dao); g_clear_error(&error);
}
/** @brief Contexte possédé pendant la révision OCR d'un RIB. */
@ -4069,8 +3858,14 @@ static void application_on_rib_ocr_confirmed(const char *iban,
}
link_dao = evidence_entity_dao_new(database, &error);
if (link_dao == NULL) goto failure;
if (!evidence_entity_dao_link(link_dao, context->evidence_identifier,
entity_identifier, &error)) goto failure;
{
gboolean exists = FALSE;
if (!evidence_entity_dao_exists(link_dao, context->evidence_identifier,
entity_identifier, &exists, &error) ||
(!exists && !evidence_entity_dao_link(link_dao,
context->evidence_identifier, entity_identifier, &error)))
goto failure;
}
if (!database_transaction_commit(database)) goto failure;
active = FALSE; project = investigation_session_get_project(application->session);
g_free(application->pending_entity_selection_identifier);
@ -5695,27 +5490,11 @@ static void application_on_evidence_selected(
application->main_window,
evidence_record
);
{
EvidenceEntityDao *observation_dao =
evidence_entity_dao_new(database, &error);
GPtrArray *observations = observation_dao != NULL
? evidence_entity_dao_list_observations(observation_dao,
evidence_identifier, &error) : NULL;
if (error == NULL)
main_window_set_evidence_observations(application->main_window,
observations);
else
g_warning("Observations EML indisponibles : %s", error->message);
g_clear_error(&error);
g_clear_pointer(&observations, g_ptr_array_unref);
evidence_entity_dao_free(observation_dao);
}
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);
@ -5727,20 +5506,11 @@ static void application_on_evidence_selected(
canonical_path[strlen(canonical_root)] == G_DIR_SEPARATOR &&
g_file_test(canonical_path, G_FILE_TEST_IS_REGULAR))
{
const char *selected_name =
evidence_record_get_original_name(evidence_record);
char *lower_name = selected_name != NULL
? g_ascii_strdown(selected_name, -1) : NULL;
eml_analysis_available = lower_name != NULL &&
g_str_has_suffix(lower_name, ".eml");
g_free(lower_name);
main_window_set_evidence_preview(application->main_window,
canonical_path,
evidence_record_get_original_name(evidence_record));
}
}
main_window_set_eml_analysis_available(
application->main_window, eml_analysis_available);
original_name =
evidence_record_get_original_name(
@ -6410,8 +6180,11 @@ static void application_on_person_evidences_selected(GPtrArray *selected,
for (guint index = 0; index < selected->len; index++)
{
const char *identifier = g_ptr_array_index(selected, index);
if (!evidence_entity_dao_link(dao, identifier,
context->entity_identifier, &error)) goto failure;
gboolean exists = FALSE;
if (!evidence_entity_dao_exists(dao, identifier,
context->entity_identifier, &exists, &error) ||
(!exists && !evidence_entity_dao_link(dao, identifier,
context->entity_identifier, &error))) goto failure;
}
if (!database_transaction_commit(database)) goto failure;
active = FALSE;
@ -8049,60 +7822,6 @@ static void application_on_reset_graph_layout_requested(
);
}
static void application_on_observation_remove_confirmed(
gboolean confirmed, gpointer user_data)
{
ApplicationObservationRemovalContext *context = user_data;
Application *application = context != NULL ? context->application : NULL;
GError *error = NULL; gboolean deleted = FALSE, shared = FALSE;
if (!confirmed || application == NULL || application->session == NULL)
{ application_observation_removal_context_free(context); return; }
if (!eml_integration_remove_promotion(
investigation_session_get_database(application->session),
context->observation_identifier, &deleted, &shared, &error))
application_present_error(application, "Retrait du graphe impossible",
error != NULL ? error->message : "La transaction a échoué.");
else
{
if (deleted)
{
const InvestigationProject *project =
investigation_session_get_project(application->session);
application_start_graph_loading(application,
investigation_project_get_database_path(project));
}
char *selected_evidence = g_strdup(
application->selected_evidence_identifier);
application_on_evidence_selected(selected_evidence, application);
g_free(selected_evidence);
main_window_set_status(application->main_window,
deleted ? "Observation conservée et nœud orphelin supprimé." :
shared ? "Observation détachée ; entité partagée conservée." :
"Observation retirée du graphe.");
}
g_clear_error(&error);
application_observation_removal_context_free(context);
}
static void application_on_observation_remove_requested(
const char *observation_identifier, gpointer user_data)
{
Application *application = user_data;
if (application == NULL ||
!g_uuid_string_is_valid(observation_identifier)) return;
ApplicationObservationRemovalContext *context =
g_new0(ApplicationObservationRemovalContext, 1);
context->application = application;
context->observation_identifier = g_strdup(observation_identifier);
application_message_dialog_present_confirmation(
main_window_get_window(application->main_window),
APPLICATION_MESSAGE_DIALOG_WARNING, "Retirer du graphe",
"Lobservation restera dans la fiche. Le nœud sera supprimé "
"uniquement sil nest utilisé par aucune autre preuve, observation "
"ou relation.", "Retirer du graphe",
application_on_observation_remove_confirmed, context);
}
/**
* @brief Ferme proprement l'application.
*
@ -8186,8 +7905,6 @@ static void application_on_activate(
application_on_evidence_selected,
application
);
main_window_set_observation_remove_callback(application->main_window,
application_on_observation_remove_requested, application);
main_window_set_verify_evidence_callback(
application->main_window,

View file

@ -3,7 +3,6 @@
* @brief Détection, normalisation et modèle de proposition bancaire (IBAN, RIB, BIC).
******************************************************************************/
#include "core/bank_proposal.h"
#include "core/iban_analyzer.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
@ -16,7 +15,6 @@ void bank_proposal_free(BankProposal *p)
g_free(p->id);
g_free(p->raw_iban);
g_free(p->normalized_iban);
g_free(p->raw_bic);
g_free(p->bic);
g_free(p->holder_name);
g_free(p->bank_name);
@ -26,7 +24,6 @@ void bank_proposal_free(BankProposal *p)
g_free(p->branch_code);
g_free(p->account_number);
g_free(p->rib_key);
g_free(p->iban_validation);
g_free(p->suggested_ocr_fix);
g_free(p->verification_status);
g_free(p->provenance_kind);
@ -37,143 +34,22 @@ void bank_proposal_free(BankProposal *p)
g_free(p);
}
static char *bank_proposal_collapse_spaces(const char *value)
{
GString *result = NULL;
gboolean previous_was_space = FALSE;
if (value == NULL)
return NULL;
result = g_string_new(NULL);
for (const char *cursor = value; *cursor != '\0'; cursor++)
{
if (g_ascii_isspace(*cursor))
{
if (!previous_was_space)
g_string_append_c(result, ' ');
previous_was_space = TRUE;
}
else
{
g_string_append_c(result, *cursor);
previous_was_space = FALSE;
}
}
g_strstrip(result->str);
return g_string_free(result, FALSE);
}
static char *bank_proposal_extract_label(
const char *text,
const char *labels_pattern
)
{
char *pattern = g_strdup_printf(
"(?im)^(?:%s)[ \\t]*:[ \\t]*(.+)$",
labels_pattern
);
GRegex *regex = g_regex_new(pattern, G_REGEX_OPTIMIZE, 0, NULL);
GMatchInfo *match = NULL;
char *raw_value = NULL;
char *value = NULL;
g_free(pattern);
g_regex_match(regex, text, 0, &match);
if (g_match_info_matches(match))
raw_value = g_match_info_fetch(match, 1);
value = bank_proposal_collapse_spaces(raw_value);
g_free(raw_value);
g_match_info_free(match);
g_regex_unref(regex);
return value;
}
static void bank_proposal_extract_bic(
BankProposal *proposal,
const char *text
)
{
GRegex *regex = g_regex_new(
"(?i)\\b[A-Z]{6}[A-Z0-9]{2}(?:[A-Z0-9]{3})?\\b",
G_REGEX_OPTIMIZE,
0,
NULL
);
GMatchInfo *match = NULL;
g_regex_match(regex, text, 0, &match);
while (g_match_info_matches(match))
{
char *candidate = g_match_info_fetch(match, 0);
char *normalized = g_ascii_strup(candidate, -1);
if (bank_proposal_validate_bic(normalized))
{
proposal->raw_bic = candidate;
proposal->bic = normalized;
break;
}
g_free(candidate);
g_free(normalized);
if (!g_match_info_next(match, NULL))
break;
}
g_match_info_free(match);
g_regex_unref(regex);
}
gboolean bank_proposal_validate_iban(const char *iban)
{
static const struct
{
const char *country_code;
gsize length;
} national_lengths[] = {
{ "BE", 16 }, { "DE", 22 }, { "ES", 24 }, { "FR", 27 },
{ "GB", 22 }, { "IT", 27 }, { "LU", 20 }, { "NL", 18 },
{ "PT", 25 }
};
char *normalized = iban_analyzer_normalize(iban);
if (normalized == NULL)
if (iban == NULL)
return FALSE;
gsize len = strlen(normalized);
gsize len = strlen(iban);
if (len < 15 || len > 34)
{
g_free(normalized);
return FALSE;
}
/* Vérification des 2 premières lettres (Code pays) */
if (!g_ascii_isalpha(normalized[0]) ||
!g_ascii_isalpha(normalized[1]) ||
!g_ascii_isdigit(normalized[2]) ||
!g_ascii_isdigit(normalized[3]))
{
g_free(normalized);
if (!g_ascii_isalpha(iban[0]) || !g_ascii_isalpha(iban[1]))
return FALSE;
}
for (guint index = 0; index < G_N_ELEMENTS(national_lengths); index++)
{
if (g_ascii_strncasecmp(
normalized,
national_lengths[index].country_code,
2
) == 0 &&
len != national_lengths[index].length)
{
g_free(normalized);
return FALSE;
}
}
/* Repositionnement des 4 premiers caractères à la fin */
GString *rearranged = g_string_new(normalized + 4);
g_string_append_len(rearranged, normalized, 4);
g_free(normalized);
GString *rearranged = g_string_new(iban + 4);
g_string_append_len(rearranged, iban, 4);
/* Conversion des lettres en chiffres (A=10, Z=35) */
GString *numeric = g_string_new(NULL);
@ -223,7 +99,12 @@ gboolean bank_proposal_validate_bic(const char *bic)
if (len != 8 && len != 11)
return FALSE;
for (gsize i = 0; i < 6; i++)
for (gsize i = 0; i < 4; i++)
{
if (!g_ascii_isalpha(bic[i]))
return FALSE;
}
for (gsize i = 4; i < 6; i++)
{
if (!g_ascii_isalpha(bic[i]))
return FALSE;
@ -263,42 +144,43 @@ gboolean bank_proposal_derive_french_rib(BankProposal *proposal)
BankProposal *bank_proposal_analyze_text(const char *raw_text, const char *evidence_id)
{
GRegex *iban_regex = NULL;
GMatchInfo *iban_match = NULL;
char *raw_iban = NULL;
char *normalized_iban = NULL;
if (raw_text == NULL || raw_text[0] == '\0')
return NULL;
iban_regex = g_regex_new(
"(?i)\\b[A-Z]{2}[0-9]{2}(?:[ \\t-]*[A-Z0-9]){11,30}\\b",
G_REGEX_OPTIMIZE,
0,
NULL
);
g_regex_match(iban_regex, raw_text, 0, &iban_match);
if (g_match_info_matches(iban_match))
raw_iban = g_match_info_fetch(iban_match, 0);
g_match_info_free(iban_match);
g_regex_unref(iban_regex);
normalized_iban = iban_analyzer_normalize(raw_iban);
if (normalized_iban == NULL)
/* Nettoyage des espaces pour recherche d'IBAN */
GString *clean = g_string_new(NULL);
gsize raw_len = strlen(raw_text);
for (gsize i = 0; i < raw_len; i++)
{
g_free(raw_iban);
char c = raw_text[i];
if (g_ascii_isalnum(c))
{
g_string_append_c(clean, g_ascii_toupper(c));
}
}
/* Recherche de motif IBAN (ex: FR76...) */
const char *data = clean->str;
const char *iban_start = strstr(data, "FR");
if (iban_start == NULL)
{
/* Essai avec d'autres codes pays à 2 lettres */
if (clean->len >= 15 && g_ascii_isalpha(data[0]) && g_ascii_isalpha(data[1]))
iban_start = data;
}
if (iban_start == NULL)
{
g_string_free(clean, TRUE);
return NULL;
}
BankProposal *p = g_new0(BankProposal, 1);
p->id = g_uuid_string_random();
p->raw_iban = raw_iban;
p->normalized_iban = normalized_iban;
p->raw_iban = g_strdup(raw_text);
p->normalized_iban = g_strndup(iban_start, 27 < strlen(iban_start) ? 27 : strlen(iban_start));
p->country_code = g_strndup(p->normalized_iban, 2);
p->is_iban_valid = bank_proposal_validate_iban(p->normalized_iban);
p->iban_validation = g_strdup(
p->is_iban_valid ? "valid" : "invalid"
);
p->verification_status = g_strdup("proposed");
p->provenance_kind = g_strdup("ocr");
p->evidence_id = evidence_id != NULL ? g_strdup(evidence_id) : NULL;
@ -315,37 +197,6 @@ BankProposal *bank_proposal_analyze_text(const char *raw_text, const char *evide
bank_proposal_derive_french_rib(p);
}
bank_proposal_extract_bic(p, raw_text);
p->holder_name = bank_proposal_extract_label(
raw_text,
"Titulaire|Account holder"
);
p->bank_name = bank_proposal_extract_label(
raw_text,
"Banque|Bank"
);
p->bank_address = bank_proposal_extract_label(
raw_text,
"Adresse(?: de la banque)?|Bank address"
);
if (!p->is_iban_valid &&
(strchr(p->normalized_iban, 'O') != NULL ||
strchr(p->normalized_iban, 'I') != NULL))
{
char *suggestion = g_strdup(p->normalized_iban);
for (char *cursor = suggestion; *cursor != '\0'; cursor++)
{
if (*cursor == 'O')
*cursor = '0';
else if (*cursor == 'I')
*cursor = '1';
}
if (bank_proposal_validate_iban(suggestion))
p->suggested_ocr_fix = suggestion;
else
g_free(suggestion);
}
g_string_free(clean, TRUE);
return p;
}

View file

@ -1,75 +0,0 @@
/******************************************************************************
* @file document_analysis.c
* @brief Modèles communs pour les analyses documentaires en mémoire.
******************************************************************************/
#include "core/document_analysis.h"
static char *document_analysis_now_utc(void)
{
GDateTime *now = g_date_time_new_now_utc();
char *value = g_date_time_format_iso8601(now);
g_date_time_unref(now);
return value;
}
DocumentToolExecution *document_tool_execution_new(
const char *tool_id,
const char *source_path
)
{
if (tool_id == NULL || tool_id[0] == '\0' ||
source_path == NULL || source_path[0] == '\0')
return NULL;
DocumentToolExecution *execution = g_new0(DocumentToolExecution, 1);
execution->tool_id = g_strdup(tool_id);
execution->source_path = g_strdup(source_path);
execution->started_at_utc = document_analysis_now_utc();
execution->arguments = g_ptr_array_new_with_free_func(g_free);
execution->warnings = g_ptr_array_new_with_free_func(g_free);
execution->errors = g_ptr_array_new_with_free_func(g_free);
execution->exit_status = -1;
execution->state = DOCUMENT_ANALYSIS_STATE_FAILED;
return execution;
}
void document_tool_execution_free(DocumentToolExecution *execution)
{
if (execution == NULL)
return;
g_free(execution->tool_id);
g_free(execution->version);
g_ptr_array_unref(execution->arguments);
g_free(execution->started_at_utc);
g_free(execution->finished_at_utc);
g_free(execution->source_path);
g_free(execution->source_sha256);
g_free(execution->raw_stdout);
g_free(execution->raw_stdout_sha256);
g_free(execution->raw_stderr);
g_ptr_array_unref(execution->warnings);
g_ptr_array_unref(execution->errors);
g_free(execution);
}
void document_tool_execution_add_argument(
DocumentToolExecution *execution,
const char *argument
)
{
if (execution != NULL && argument != NULL)
g_ptr_array_add(execution->arguments, g_strdup(argument));
}
const char *document_analysis_state_code(DocumentAnalysisState state)
{
switch (state)
{
case DOCUMENT_ANALYSIS_STATE_SUCCESS: return "success";
case DOCUMENT_ANALYSIS_STATE_PARTIAL: return "partial";
case DOCUMENT_ANALYSIS_STATE_UNAVAILABLE: return "unavailable";
case DOCUMENT_ANALYSIS_STATE_CANCELLED: return "cancelled";
case DOCUMENT_ANALYSIS_STATE_INCOMPATIBLE: return "incompatible";
case DOCUMENT_ANALYSIS_STATE_FAILED: return "failed";
}
return "failed";
}

View file

@ -1,101 +0,0 @@
/******************************************************************************
* @file document_file_analysis.c
* @brief Orchestration des analyses compatibles d'un fichier dérivé.
******************************************************************************/
#include "core/document_file_analysis.h"
void document_file_analysis_free(DocumentFileAnalysis *analysis)
{
if (analysis == NULL)
return;
g_free(analysis->source_path);
g_free(analysis->declared_mime);
g_free(analysis->detected_mime);
exiftool_analysis_result_free(analysis->metadata);
ocr_analysis_result_free(analysis->ocr);
pdf_analysis_result_free(analysis->pdf);
g_ptr_array_unref(analysis->warnings);
g_free(analysis);
}
static const char *document_file_analysis_effective_mime(
const char *declared_mime,
const char *detected_mime
)
{
return detected_mime != NULL && detected_mime[0] != '\0'
? detected_mime
: declared_mime;
}
DocumentFileAnalysis *document_file_analysis_run(
const DocumentAnalysisTools *tools,
const char *source_path,
const char *declared_mime,
const char *detected_mime,
gboolean request_image_ocr,
const char *ocr_languages,
GCancellable *cancellable,
GError **error
)
{
if (tools == NULL || source_path == NULL)
{
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
"Les paramètres d'analyse du fichier sont invalides.");
return NULL;
}
DocumentFileAnalysis *analysis = g_new0(DocumentFileAnalysis, 1);
analysis->source_path = g_strdup(source_path);
analysis->declared_mime = g_strdup(declared_mime);
analysis->detected_mime = g_strdup(detected_mime);
analysis->warnings = g_ptr_array_new_with_free_func(g_free);
analysis->state = DOCUMENT_ANALYSIS_STATE_SUCCESS;
const char *mime = document_file_analysis_effective_mime(
declared_mime, detected_mime);
analysis->metadata = exiftool_analysis_run(
tools->exiftool, source_path, cancellable, error);
if (analysis->metadata == NULL)
{
if (error != NULL && *error != NULL &&
g_error_matches(*error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
goto failure;
g_clear_error(error);
analysis->state = DOCUMENT_ANALYSIS_STATE_PARTIAL;
}
else if (analysis->metadata->execution->state !=
DOCUMENT_ANALYSIS_STATE_SUCCESS)
analysis->state = DOCUMENT_ANALYSIS_STATE_PARTIAL;
if (g_strcmp0(mime, "application/pdf") == 0)
{
PdfAnalysisTools pdf_tools = {
.pdfinfo = tools->pdfinfo,
.pdftotext = tools->pdftotext,
.pdftoppm = tools->pdftoppm,
.tesseract = tools->tesseract
};
analysis->pdf = pdf_analysis_run(&pdf_tools, source_path,
ocr_languages, cancellable, error);
if (analysis->pdf == NULL)
goto failure;
if (analysis->pdf->state != DOCUMENT_ANALYSIS_STATE_SUCCESS)
analysis->state = DOCUMENT_ANALYSIS_STATE_PARTIAL;
}
else if (request_image_ocr && ocr_analysis_mime_is_compatible(mime))
{
analysis->ocr = ocr_analysis_run(tools->tesseract,
source_path, ocr_languages, cancellable, error);
if (analysis->ocr == NULL)
goto failure;
if (analysis->ocr->execution->state !=
DOCUMENT_ANALYSIS_STATE_SUCCESS)
analysis->state = DOCUMENT_ANALYSIS_STATE_PARTIAL;
}
return analysis;
failure:
document_file_analysis_free(analysis);
return NULL;
}

View file

@ -1,327 +0,0 @@
/******************************************************************************
* @file document_tool_runner.c
* @brief Exécution réellement bornée et annulable des outils documentaires.
******************************************************************************/
#include "core/document_tool_runner.h"
#include "core/file_hash.h"
#include <glib/gstdio.h>
#define DOCUMENT_TOOL_RUNNER_READ_BLOCK 4096U
#define DOCUMENT_TOOL_RUNNER_VERSION_LIMIT 65536U
typedef struct
{
GInputStream *stream;
GCancellable *cancellable;
GByteArray *prefix;
gsize limit;
gsize bytes_observed;
gboolean truncated;
GError *error;
} DocumentToolStreamCapture;
typedef struct
{
GBytes *stdout_bytes;
GBytes *stderr_bytes;
gsize stdout_bytes_observed;
gsize stderr_bytes_observed;
gboolean stdout_truncated;
gboolean stderr_truncated;
gboolean exited_normally;
int exit_status;
} DocumentToolCaptureResult;
static GPtrArray *document_tool_runner_build_argv(
const char *executable,
const char *const arguments[]
)
{
GPtrArray *argv = g_ptr_array_new();
g_ptr_array_add(argv, (gpointer) executable);
for (gsize index = 0; arguments != NULL &&
arguments[index] != NULL; index++)
g_ptr_array_add(argv, (gpointer) arguments[index]);
g_ptr_array_add(argv, NULL);
return argv;
}
static gpointer document_tool_runner_drain_stream(gpointer user_data)
{
DocumentToolStreamCapture *capture = user_data;
guint8 block[DOCUMENT_TOOL_RUNNER_READ_BLOCK];
while (TRUE)
{
gssize bytes_read = g_input_stream_read(
capture->stream, block, sizeof(block),
capture->cancellable, &capture->error);
if (bytes_read <= 0)
break;
gsize observed = (gsize) bytes_read;
if (G_MAXSIZE - capture->bytes_observed < observed)
capture->bytes_observed = G_MAXSIZE;
else
capture->bytes_observed += observed;
gsize remaining = capture->prefix->len < capture->limit
? capture->limit - capture->prefix->len : 0;
gsize retained = MIN(remaining, observed);
if (retained > 0)
g_byte_array_append(capture->prefix, block, retained);
if (retained < observed)
capture->truncated = TRUE;
}
return NULL;
}
static void document_tool_capture_result_clear(
DocumentToolCaptureResult *result
)
{
g_clear_pointer(&result->stdout_bytes, g_bytes_unref);
g_clear_pointer(&result->stderr_bytes, g_bytes_unref);
}
static gboolean document_tool_runner_capture(
const char *executable,
const char *const arguments[],
const DocumentToolRunnerLimits *limits,
GCancellable *cancellable,
DocumentToolCaptureResult *out_result,
GError **error
)
{
GPtrArray *argv = document_tool_runner_build_argv(executable, arguments);
GSubprocessLauncher *launcher = g_subprocess_launcher_new(
G_SUBPROCESS_FLAGS_STDOUT_PIPE | G_SUBPROCESS_FLAGS_STDERR_PIPE);
GError *local_error = NULL;
GSubprocess *process = g_subprocess_launcher_spawnv(
launcher, (const char *const *) argv->pdata, &local_error);
g_object_unref(launcher);
g_ptr_array_unref(argv);
if (process == NULL)
{
g_propagate_error(error, local_error);
return FALSE;
}
DocumentToolStreamCapture stdout_capture = {
.stream = g_subprocess_get_stdout_pipe(process),
.cancellable = cancellable,
.prefix = g_byte_array_sized_new(MIN(limits->stdout_limit, 4096U)),
.limit = limits->stdout_limit
};
DocumentToolStreamCapture stderr_capture = {
.stream = g_subprocess_get_stderr_pipe(process),
.cancellable = cancellable,
.prefix = g_byte_array_sized_new(MIN(limits->stderr_limit, 4096U)),
.limit = limits->stderr_limit
};
GThread *stdout_thread = g_thread_new(
"document-stdout", document_tool_runner_drain_stream, &stdout_capture);
GThread *stderr_thread = g_thread_new(
"document-stderr", document_tool_runner_drain_stream, &stderr_capture);
gboolean waited = g_subprocess_wait(process, cancellable, &local_error);
if (!waited)
{
g_subprocess_force_exit(process);
GError *final_wait_error = NULL;
(void) g_subprocess_wait(process, NULL, &final_wait_error);
g_clear_error(&final_wait_error);
}
g_thread_join(stdout_thread);
g_thread_join(stderr_thread);
gboolean cancelled =
(cancellable != NULL && g_cancellable_is_cancelled(cancellable)) ||
g_error_matches(local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED) ||
g_error_matches(stdout_capture.error, G_IO_ERROR, G_IO_ERROR_CANCELLED) ||
g_error_matches(stderr_capture.error, G_IO_ERROR, G_IO_ERROR_CANCELLED);
if (!waited || stdout_capture.error != NULL ||
stderr_capture.error != NULL)
{
if (cancelled)
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_CANCELLED,
"L'exécution de l'outil documentaire a été annulée.");
else if (local_error != NULL)
g_propagate_error(error, g_steal_pointer(&local_error));
else
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_FAILED,
"La lecture des sorties de l'outil documentaire a échoué.");
g_clear_error(&local_error);
g_clear_error(&stdout_capture.error);
g_clear_error(&stderr_capture.error);
g_byte_array_unref(stdout_capture.prefix);
g_byte_array_unref(stderr_capture.prefix);
g_object_unref(process);
return FALSE;
}
out_result->stdout_bytes = g_byte_array_free_to_bytes(
stdout_capture.prefix);
out_result->stderr_bytes = g_byte_array_free_to_bytes(
stderr_capture.prefix);
out_result->stdout_bytes_observed = stdout_capture.bytes_observed;
out_result->stderr_bytes_observed = stderr_capture.bytes_observed;
out_result->stdout_truncated = stdout_capture.truncated;
out_result->stderr_truncated = stderr_capture.truncated;
out_result->exited_normally = g_subprocess_get_if_exited(process);
out_result->exit_status = out_result->exited_normally
? g_subprocess_get_exit_status(process) : -1;
g_clear_error(&local_error);
g_object_unref(process);
return TRUE;
}
static char *document_tool_runner_bytes_to_text(GBytes *bytes)
{
gsize length = 0;
const char *data = g_bytes_get_data(bytes, &length);
return g_utf8_make_valid(data != NULL ? data : "", (gssize) length);
}
char *document_tool_runner_read_version(
const char *executable,
const char *const arguments[],
GCancellable *cancellable
)
{
DocumentToolRunnerLimits limits = {
DOCUMENT_TOOL_RUNNER_VERSION_LIMIT,
DOCUMENT_TOOL_RUNNER_VERSION_LIMIT
};
DocumentToolCaptureResult capture = { 0 };
GError *error = NULL;
if (!document_tool_runner_capture(executable, arguments, &limits,
cancellable, &capture, &error))
{
g_clear_error(&error);
return NULL;
}
char *version = document_tool_runner_bytes_to_text(
g_bytes_get_size(capture.stdout_bytes) > 0
? capture.stdout_bytes : capture.stderr_bytes);
g_strstrip(version);
document_tool_capture_result_clear(&capture);
return version;
}
gboolean document_tool_runner_run_with_limits(
const char *tool_id,
const char *executable,
const char *const arguments[],
const char *source_path,
const DocumentToolRunnerLimits *limits,
GCancellable *cancellable,
DocumentToolExecution **out_execution,
GError **error
)
{
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
if (tool_id == NULL || executable == NULL || source_path == NULL ||
limits == NULL || limits->stdout_limit == 0 ||
limits->stderr_limit == 0 || out_execution == NULL ||
*out_execution != NULL)
{
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
"Les paramètres de l'outil documentaire sont invalides.");
return FALSE;
}
GStatBuf source_stat;
if (g_stat(source_path, &source_stat) == 0 &&
source_stat.st_size > DOCUMENT_ANALYSIS_MAX_FILE_SIZE)
{
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_NO_SPACE,
"Le fichier dépasse la taille maximale d'analyse.");
return FALSE;
}
DocumentToolExecution *execution =
document_tool_execution_new(tool_id, source_path);
for (gsize index = 0; arguments != NULL &&
arguments[index] != NULL; index++)
document_tool_execution_add_argument(execution, arguments[index]);
(void) file_hash_compute_sha256(source_path, cancellable,
&execution->source_sha256, NULL, NULL);
DocumentToolCaptureResult capture = { 0 };
GError *capture_error = NULL;
if (!document_tool_runner_capture(executable, arguments, limits,
cancellable, &capture, &capture_error))
{
if (g_error_matches(capture_error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
{
execution->state = DOCUMENT_ANALYSIS_STATE_CANCELLED;
g_propagate_error(error, capture_error);
capture_error = NULL;
}
else
{
execution->state = DOCUMENT_ANALYSIS_STATE_UNAVAILABLE;
g_ptr_array_add(execution->errors, g_strdup(
capture_error != NULL ? capture_error->message :
"Outil indisponible."));
}
g_clear_error(&capture_error);
GDateTime *now = g_date_time_new_now_utc();
execution->finished_at_utc = g_date_time_format_iso8601(now);
g_date_time_unref(now);
*out_execution = execution;
return execution->state != DOCUMENT_ANALYSIS_STATE_CANCELLED;
}
execution->raw_stdout =
document_tool_runner_bytes_to_text(capture.stdout_bytes);
execution->raw_stderr =
document_tool_runner_bytes_to_text(capture.stderr_bytes);
execution->stdout_bytes_observed = capture.stdout_bytes_observed;
execution->stderr_bytes_observed = capture.stderr_bytes_observed;
execution->stdout_truncated = capture.stdout_truncated;
execution->stderr_truncated = capture.stderr_truncated;
execution->exit_status = capture.exit_status;
execution->raw_stdout_sha256 = g_compute_checksum_for_string(
G_CHECKSUM_SHA256, execution->raw_stdout, -1);
if (execution->stdout_truncated)
g_ptr_array_add(execution->warnings,
g_strdup("La sortie standard de l'outil a été tronquée."));
if (execution->stderr_truncated)
g_ptr_array_add(execution->warnings,
g_strdup("La sortie d'erreur de l'outil a été tronquée."));
execution->state =
execution->stdout_truncated || execution->stderr_truncated
? DOCUMENT_ANALYSIS_STATE_PARTIAL
: capture.exited_normally && capture.exit_status == 0
? DOCUMENT_ANALYSIS_STATE_SUCCESS
: DOCUMENT_ANALYSIS_STATE_FAILED;
GDateTime *now = g_date_time_new_now_utc();
execution->finished_at_utc = g_date_time_format_iso8601(now);
g_date_time_unref(now);
document_tool_capture_result_clear(&capture);
*out_execution = execution;
return TRUE;
}
gboolean document_tool_runner_run(
const char *tool_id,
const char *executable,
const char *const arguments[],
const char *source_path,
GCancellable *cancellable,
DocumentToolExecution **out_execution,
GError **error
)
{
const DocumentToolRunnerLimits limits = {
DOCUMENT_ANALYSIS_MAX_STDOUT,
DOCUMENT_ANALYSIS_MAX_STDERR
};
return document_tool_runner_run_with_limits(tool_id, executable,
arguments, source_path, &limits, cancellable, out_execution, error);
}

View file

@ -3,8 +3,6 @@
* @brief Analyse locale et non destructive des en-têtes d'un fichier EML.
******************************************************************************/
#include "core/eml_analyzer.h"
#include <gio/gio.h>
#include <stdio.h>
#include <string.h>
#define EML_ANALYZER_MAX_FILE_SIZE (25U * 1024U * 1024U)
#define EML_ANALYZER_MAX_HEADER_SIZE (2U * 1024U * 1024U)
@ -16,132 +14,8 @@ struct EmlAnalysis
GPtrArray *ips;
GPtrArray *sender_ips;
GPtrArray *destination_ips;
GPtrArray *observations;
char *raw_headers;
char *date_utc;
};
void eml_observation_free(EmlObservation *observation)
{
if (observation == NULL) return;
g_free(observation->type_identifier);
g_free(observation->value_raw);
g_free(observation->value_normalized);
g_free(observation->role);
g_free(observation->source_header);
g_free(observation->provenance_kind);
g_free(observation);
}
static void eml_analyzer_add_observation(EmlAnalysis *analysis,
const char *type, const char *raw, const char *normalized,
const char *role, const char *header, guint occurrence)
{
EmlObservation *observation = g_new0(EmlObservation, 1);
observation->type_identifier = g_strdup(type);
observation->value_raw = g_strdup(raw);
observation->value_normalized = g_strdup(normalized);
observation->role = g_strdup(role);
observation->source_header = g_strdup(header);
observation->occurrence = occurrence;
observation->provenance_kind = g_strdup("header");
g_ptr_array_add(analysis->observations, observation);
}
static gint eml_analyzer_month_number(const char *month)
{
static const char *months[] = {
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};
for (guint index = 0; index < G_N_ELEMENTS(months); index++)
if (g_ascii_strcasecmp(month, months[index]) == 0)
return (gint) index + 1;
return 0;
}
static char *eml_analyzer_normalize_date(const char *raw_date)
{
char month_name[4] = { 0 };
char timezone_text[6] = { 0 };
const char *date_start = raw_date;
gint day = 0;
gint year = 0;
gint hour = 0;
gint minute = 0;
gint second = 0;
gint month = 0;
gint parsed = 0;
char timezone_identifier[7] = { 0 };
GTimeZone *timezone = NULL;
GDateTime *date = NULL;
GDateTime *utc_date = NULL;
char *result = NULL;
if (raw_date == NULL)
return NULL;
const char *comma = strchr(raw_date, ',');
if (comma != NULL)
date_start = comma + 1;
parsed = sscanf(
date_start,
" %d %3s %d %d:%d:%d %5s",
&day,
month_name,
&year,
&hour,
&minute,
&second,
timezone_text
);
if (parsed != 7 ||
strlen(timezone_text) != 5 ||
(timezone_text[0] != '+' && timezone_text[0] != '-') ||
!g_ascii_isdigit(timezone_text[1]) ||
!g_ascii_isdigit(timezone_text[2]) ||
!g_ascii_isdigit(timezone_text[3]) ||
!g_ascii_isdigit(timezone_text[4]))
return NULL;
month = eml_analyzer_month_number(month_name);
if (month == 0)
return NULL;
g_snprintf(
timezone_identifier,
sizeof(timezone_identifier),
"%c%c%c:%c%c",
timezone_text[0],
timezone_text[1],
timezone_text[2],
timezone_text[3],
timezone_text[4]
);
timezone = g_time_zone_new_identifier(timezone_identifier);
if (timezone == NULL)
return NULL;
date = g_date_time_new(
timezone,
year,
month,
day,
hour,
minute,
(gdouble) second
);
g_time_zone_unref(timezone);
if (date == NULL)
return NULL;
utc_date = g_date_time_to_utc(date);
result = g_date_time_format(utc_date, "%Y-%m-%dT%H:%M:%SZ");
g_date_time_unref(utc_date);
g_date_time_unref(date);
return result;
}
/** @brief Libère un tableau de valeurs d'en-tête. */
static void eml_analyzer_values_free(gpointer data)
{
@ -180,129 +54,6 @@ static void eml_analyzer_extract_regex(GRegex *regex, const char *text,
}
g_match_info_free(matches);
}
static gboolean eml_analyzer_valid_domain(const char *value)
{
gboolean has_dot = FALSE;
const char *label = value;
GInetAddress *address = value != NULL
? g_inet_address_new_from_string(value) : NULL;
if (value == NULL) return FALSE;
if (address != NULL) { g_object_unref(address); return FALSE; }
for (const char *cursor = value; *cursor != '\0'; cursor++)
{
if (*cursor == '.')
{
if (cursor == label || cursor[-1] == '-') return FALSE;
has_dot = TRUE; label = cursor + 1;
}
else if (!(g_ascii_isalnum(*cursor) || *cursor == '-') ||
(cursor == label && *cursor == '-')) return FALSE;
}
if (!has_dot || *label == '\0' || label[strlen(label) - 1] == '-')
return FALSE;
for (const char *cursor = label; *cursor != '\0'; cursor++)
if (g_ascii_isalpha(*cursor)) return strlen(label) >= 2;
return FALSE;
}
static void eml_analyzer_observe_email_header(EmlAnalysis *analysis,
GRegex *email_regex, const char *header, const char *role)
{
const GPtrArray *values = eml_analysis_get_header_values(analysis, header);
for (guint index = 0; values != NULL && index < values->len; index++)
{
const char *value = g_ptr_array_index((GPtrArray *) values, index);
GMatchInfo *matches = NULL;
g_regex_match(email_regex, value, 0, &matches);
while (g_match_info_matches(matches))
{
char *raw = g_match_info_fetch(matches, 1);
char *normalized = g_ascii_strdown(raw, -1);
eml_analyzer_add_unique(analysis->emails, normalized, FALSE);
eml_analyzer_add_observation(analysis, "email_address", raw,
normalized, role, header, index + 1);
const char *at = strrchr(normalized, '@');
if (at != NULL && eml_analyzer_valid_domain(at + 1))
{
eml_analyzer_add_unique(analysis->domains, at + 1, TRUE);
eml_analyzer_add_observation(analysis, "domain_name", at + 1,
at + 1, role, header, index + 1);
}
g_free(normalized); g_free(raw);
if (!g_match_info_next(matches, NULL)) break;
}
g_match_info_free(matches);
}
}
static void eml_analyzer_observe_received(EmlAnalysis *analysis,
GRegex *ip_regex, GRegex *host_regex)
{
const GPtrArray *values = eml_analysis_get_header_values(analysis, "received");
for (guint index = 0; values != NULL && index < values->len; index++)
{
const char *value = g_ptr_array_index((GPtrArray *) values, index);
GMatchInfo *matches = NULL;
g_regex_match(ip_regex, value, 0, &matches);
while (g_match_info_matches(matches))
{
char *raw = g_match_info_fetch(matches, 1);
GInetAddress *address = g_inet_address_new_from_string(raw);
if (address != NULL)
{
char *normalized = g_inet_address_to_string(address);
eml_analyzer_add_unique(analysis->ips, normalized, FALSE);
eml_analyzer_add_observation(analysis, "ip_address", raw,
normalized, "smtp_relay", "received", index + 1);
g_free(normalized); g_object_unref(address);
}
g_free(raw);
if (!g_match_info_next(matches, NULL)) break;
}
g_match_info_free(matches);
g_regex_match(host_regex, value, 0, &matches);
while (g_match_info_matches(matches))
{
char *raw = g_match_info_fetch(matches, 1);
char *normalized = g_ascii_strdown(raw, -1);
if (eml_analyzer_valid_domain(normalized))
{
eml_analyzer_add_unique(analysis->domains, normalized, FALSE);
eml_analyzer_add_observation(analysis, "domain_name", raw,
normalized, "smtp_relay", "received", index + 1);
}
g_free(normalized); g_free(raw);
if (!g_match_info_next(matches, NULL)) break;
}
g_match_info_free(matches);
}
}
static void eml_analyzer_observe_received_ipv6(EmlAnalysis *analysis,
GRegex *regex)
{
const GPtrArray *values = eml_analysis_get_header_values(analysis, "received");
for (guint index = 0; values != NULL && index < values->len; index++)
{
const char *value = g_ptr_array_index((GPtrArray *) values, index);
GMatchInfo *matches = NULL;
g_regex_match(regex, value, 0, &matches);
while (g_match_info_matches(matches))
{
char *raw = g_match_info_fetch(matches, 1);
GInetAddress *address = g_inet_address_new_from_string(raw);
if (address != NULL &&
g_inet_address_get_family(address) == G_SOCKET_FAMILY_IPV6)
{
char *normalized = g_inet_address_to_string(address);
eml_analyzer_add_unique(analysis->ips, normalized, FALSE);
eml_analyzer_add_observation(analysis, "ip_address", raw,
normalized, "smtp_relay", "received", index + 1);
g_free(normalized);
}
g_clear_object(&address); g_free(raw);
if (!g_match_info_next(matches, NULL)) break;
}
g_match_info_free(matches);
}
}
/** @brief Extrait les IP d'une portion nommée d'un en-tête Received. */
static void eml_analyzer_extract_received_part(GRegex *part_regex,
GRegex *ip_regex, const char *received, GPtrArray *values)
@ -349,7 +100,6 @@ EmlAnalysis *eml_analyzer_analyze_file(const char *file_path, GError **error)
char *utf8 = NULL, **lines = NULL, *current_name = NULL;
GString *current_value = NULL;
GRegex *email_regex = NULL, *domain_regex = NULL, *ip_regex = NULL;
GRegex *ipv6_regex = NULL;
GRegex *received_from_regex = NULL, *received_by_regex = NULL;
g_return_val_if_fail(error == NULL || *error == NULL, NULL);
if (file_path == NULL || file_path[0] == '\0')
@ -377,8 +127,6 @@ EmlAnalysis *eml_analyzer_analyze_file(const char *file_path, GError **error)
analysis->ips = g_ptr_array_new_with_free_func(g_free);
analysis->sender_ips = g_ptr_array_new_with_free_func(g_free);
analysis->destination_ips = g_ptr_array_new_with_free_func(g_free);
analysis->observations = g_ptr_array_new_with_free_func(
(GDestroyNotify) eml_observation_free);
analysis->raw_headers = g_utf8_make_valid(data, (gssize) header_size);
utf8 = g_strdup(analysis->raw_headers); lines = g_strsplit(utf8, "\n", -1);
current_value = g_string_new(NULL);
@ -398,42 +146,13 @@ EmlAnalysis *eml_analyzer_analyze_file(const char *file_path, GError **error)
if (current_name != NULL) eml_analyzer_add_header(analysis,
current_name, current_value->str);
email_regex = g_regex_new("([A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,})", 0, 0, NULL);
domain_regex = g_regex_new("(?i)\\b([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+)\\b", 0, 0, NULL);
domain_regex = g_regex_new("(?:@|[.\\s<([])([A-Za-z0-9-]+(?:\\.[A-Za-z0-9-]+)+)", 0, 0, NULL);
ip_regex = g_regex_new("(?:^|[^0-9])([0-9]{1,3}(?:\\.[0-9]{1,3}){3})(?:[^0-9]|$)", 0, 0, NULL);
ipv6_regex = g_regex_new("(?i)(?:\\[)?([0-9a-f]*:[0-9a-f:]+)(?:\\])?", 0, 0, NULL);
received_from_regex = g_regex_new("(?i)\\bfrom\\b(.*?)\\bby\\b", 0, 0, NULL);
received_by_regex = g_regex_new("(?i)\\bby\\b(.*?)(?:\\bwith\\b|;|$)", 0, 0, NULL);
static const struct { const char *header; const char *role; } email_headers[] = {
{ "from", "from" }, { "sender", "sender" },
{ "reply-to", "reply_to" }, { "return-path", "return_path" },
{ "to", "to" }, { "cc", "cc" }, { "bcc", "bcc" }
};
for (guint i = 0; i < G_N_ELEMENTS(email_headers); i++)
eml_analyzer_observe_email_header(analysis, email_regex,
email_headers[i].header, email_headers[i].role);
const char *message_id = eml_analysis_get_first_header(analysis, "message-id");
if (message_id != NULL)
{
GMatchInfo *matches = NULL;
g_regex_match(email_regex, message_id, 0, &matches);
if (g_match_info_matches(matches))
{
char *identifier = g_match_info_fetch(matches, 1);
const char *at = strrchr(identifier, '@');
if (at != NULL && eml_analyzer_valid_domain(at + 1))
{
char *normalized = g_ascii_strdown(at + 1, -1);
eml_analyzer_add_unique(analysis->domains, normalized, FALSE);
eml_analyzer_add_observation(analysis, "domain_name", at + 1,
normalized, "message_id_domain", "message-id", 1);
g_free(normalized);
}
g_free(identifier);
}
g_match_info_free(matches);
}
eml_analyzer_observe_received(analysis, ip_regex, domain_regex);
eml_analyzer_observe_received_ipv6(analysis, ipv6_regex);
eml_analyzer_extract_regex(email_regex, analysis->raw_headers, analysis->emails, TRUE);
eml_analyzer_extract_regex(domain_regex, analysis->raw_headers, analysis->domains, TRUE);
eml_analyzer_extract_regex(ip_regex, analysis->raw_headers, analysis->ips, FALSE);
const GPtrArray *received_values = eml_analysis_get_header_values(
analysis, "received");
for (guint i = 0; received_values != NULL && i < received_values->len; i++)
@ -445,13 +164,9 @@ EmlAnalysis *eml_analyzer_analyze_file(const char *file_path, GError **error)
eml_analyzer_extract_received_part(received_by_regex, ip_regex,
received, analysis->destination_ips);
}
analysis->date_utc = eml_analyzer_normalize_date(
eml_analysis_get_first_header(analysis, "date")
);
cleanup:
g_clear_pointer(&email_regex, g_regex_unref); g_clear_pointer(&domain_regex, g_regex_unref);
g_clear_pointer(&ip_regex, g_regex_unref); g_clear_pointer(&current_name, g_free);
g_clear_pointer(&ipv6_regex, g_regex_unref);
g_clear_pointer(&received_from_regex, g_regex_unref);
g_clear_pointer(&received_by_regex, g_regex_unref);
if (current_value != NULL) g_string_free(current_value, TRUE);
@ -465,8 +180,7 @@ void eml_analysis_free(EmlAnalysis *analysis)
g_ptr_array_unref(analysis->domains); g_ptr_array_unref(analysis->ips);
g_ptr_array_unref(analysis->sender_ips);
g_ptr_array_unref(analysis->destination_ips);
g_ptr_array_unref(analysis->observations);
g_free(analysis->raw_headers); g_free(analysis->date_utc); g_free(analysis);
g_free(analysis->raw_headers); g_free(analysis);
}
const GPtrArray *eml_analysis_get_header_values(const EmlAnalysis *analysis,
const char *name)
@ -484,9 +198,7 @@ const char *eml_analysis_get_first_header(const EmlAnalysis *analysis,
}
const GPtrArray *eml_analysis_get_email_addresses(const EmlAnalysis *a) { return a != NULL ? a->emails : NULL; }
const GPtrArray *eml_analysis_get_domains(const EmlAnalysis *a) { return a != NULL ? a->domains : NULL; }
const GPtrArray *eml_analysis_get_observations(const EmlAnalysis *a) { return a != NULL ? a->observations : NULL; }
const GPtrArray *eml_analysis_get_ip_addresses(const EmlAnalysis *a) { return a != NULL ? a->ips : NULL; }
const GPtrArray *eml_analysis_get_sender_ip_addresses(const EmlAnalysis *a) { return a != NULL ? a->sender_ips : NULL; }
const GPtrArray *eml_analysis_get_destination_ip_addresses(const EmlAnalysis *a) { return a != NULL ? a->destination_ips : NULL; }
const char *eml_analysis_get_raw_headers(const EmlAnalysis *a) { return a != NULL ? a->raw_headers : NULL; }
const char *eml_analysis_get_date_utc(const EmlAnalysis *a) { return a != NULL ? a->date_utc : NULL; }

View file

@ -7,7 +7,6 @@
#include "dao/entity_dao.h"
#include "dao/evidence_entity_dao.h"
#include "database/transaction.h"
#include "database/statement.h"
#include "models/entity_record.h"
EmlEntityProposal *eml_entity_proposal_new(const char *type, const char *value)
@ -27,40 +26,16 @@ EmlEntityProposal *eml_entity_proposal_new_with_metadata(const char *type,
? verification_status : "proposed");
proposal->provenance_kind = g_strdup(provenance_kind != NULL
? provenance_kind : "header");
proposal->value_raw = g_strdup(value);
proposal->role = g_strdup("other");
proposal->source_header = g_strdup("manual");
proposal->occurrence = 1;
if (proposal->type_identifier == NULL || proposal->value == NULL ||
proposal->verification_status == NULL || proposal->provenance_kind == NULL)
{ eml_entity_proposal_free(proposal); return NULL; }
return proposal;
}
EmlEntityProposal *eml_entity_proposal_new_observation(const char *type,
const char *raw, const char *normalized, const char *role,
const char *source_header, guint occurrence,
const char *verification_status, const char *provenance_kind)
{
EmlEntityProposal *proposal = eml_entity_proposal_new_with_metadata(type,
normalized, verification_status, provenance_kind);
if (proposal == NULL) return NULL;
g_free(proposal->value_raw); proposal->value_raw = g_strdup(raw);
g_free(proposal->role); proposal->role = g_strdup(role);
g_free(proposal->source_header);
proposal->source_header = g_strdup(source_header);
proposal->occurrence = occurrence;
if (proposal->value_raw == NULL || proposal->role == NULL ||
proposal->source_header == NULL || occurrence == 0)
{ eml_entity_proposal_free(proposal); return NULL; }
return proposal;
}
void eml_entity_proposal_free(EmlEntityProposal *proposal)
{
if (proposal == NULL) return;
g_free(proposal->type_identifier); g_free(proposal->value);
g_free(proposal->verification_status); g_free(proposal->provenance_kind);
g_free(proposal->value_raw); g_free(proposal->role);
g_free(proposal->source_header);
g_free(proposal);
}
/** @brief Recherche une entité existante avec le même type et la même valeur. */
@ -78,8 +53,7 @@ static const EntityRecord *eml_integration_find_existing(const GPtrArray *entiti
return NULL;
}
gboolean eml_integration_apply(Database *database, const char *evidence_identifier,
const GPtrArray *proposals, guint *out_observations,
guint *out_created, guint *out_reused, GError **error)
const GPtrArray *proposals, guint *out_created, guint *out_reused, GError **error)
{
EntityDao *entity_dao = NULL;
EvidenceEntityDao *link_dao = NULL;
@ -88,7 +62,6 @@ gboolean eml_integration_apply(Database *database, const char *evidence_identifi
char *timestamp = NULL;
guint created = 0;
guint reused = 0;
guint observations = 0;
gboolean active = FALSE;
gboolean success = FALSE;
@ -98,8 +71,6 @@ gboolean eml_integration_apply(Database *database, const char *evidence_identifi
*out_created = 0;
if (out_reused != NULL)
*out_reused = 0;
if (out_observations != NULL)
*out_observations = 0;
if (database == NULL || evidence_identifier == NULL || proposals == NULL || proposals->len == 0)
{
@ -113,10 +84,6 @@ gboolean eml_integration_apply(Database *database, const char *evidence_identifi
const EmlEntityProposal *proposal = g_ptr_array_index(
(GPtrArray *) proposals, i);
if (proposal == NULL ||
proposal->role == NULL || proposal->role[0] == '\0' ||
proposal->source_header == NULL ||
proposal->source_header[0] == '\0' ||
proposal->occurrence == 0 ||
!controlled_vocab_is_valid_verification_status(
proposal->verification_status) ||
!controlled_vocab_is_valid_provenance_kind(
@ -149,21 +116,11 @@ gboolean eml_integration_apply(Database *database, const char *evidence_identifi
for (guint i = 0; i < proposals->len; i++)
{
EmlEntityProposal *proposal = g_ptr_array_index((GPtrArray *) proposals, i);
char *observation_identifier = NULL;
if (!evidence_entity_dao_add_observation(link_dao,
evidence_identifier, proposal->type_identifier,
proposal->value_raw, proposal->value, proposal->role,
proposal->provenance_kind, proposal->source_header,
proposal->occurrence, proposal->verification_status,
timestamp, &observation_identifier, error))
goto cleanup;
observations++;
if (!proposal->promote_to_entity)
{ g_free(observation_identifier); continue; }
const EntityRecord *existing = eml_integration_find_existing(entities, proposal);
const char *identifier = existing != NULL
? entity_record_get_identifier(existing) : NULL;
char *new_identifier = NULL; EntityRecord *new_record = NULL;
gboolean linked = FALSE;
if (existing == NULL)
{
new_identifier = g_uuid_string_random(); identifier = new_identifier;
@ -172,27 +129,21 @@ gboolean eml_integration_apply(Database *database, const char *evidence_identifi
"Indicateur extrait des en-têtes de la preuve EML.", 50,
timestamp, timestamp, ENTITY_STATUS_ACTIVE, error);
if (new_record == NULL || !entity_dao_insert(entity_dao, new_record, error))
{ entity_record_free(new_record); g_free(observation_identifier);
g_free(new_identifier); goto cleanup; }
{ entity_record_free(new_record); g_free(new_identifier); goto cleanup; }
g_ptr_array_add(entities, new_record); new_record = NULL; created++;
}
else reused++;
if (!evidence_entity_dao_add_source(link_dao, evidence_identifier,
identifier, "eml_observation", observation_identifier,
timestamp, error))
{ g_free(observation_identifier); g_free(new_identifier); goto cleanup; }
if (!evidence_entity_dao_promote_observation(link_dao,
observation_identifier, identifier, timestamp,
existing != NULL ? "reused" : "created", error))
{ g_free(observation_identifier); g_free(new_identifier); goto cleanup; }
g_free(observation_identifier);
if (!evidence_entity_dao_exists(link_dao, evidence_identifier,
identifier, &linked, error) ||
(!linked && !evidence_entity_dao_link(link_dao,
evidence_identifier, identifier, error)))
{ g_free(new_identifier); goto cleanup; }
g_free(new_identifier);
}
if (!database_transaction_commit(database)) goto cleanup;
active = FALSE; success = TRUE;
if (out_created != NULL) *out_created = created;
if (out_reused != NULL) *out_reused = reused;
if (out_observations != NULL) *out_observations = observations;
cleanup:
if (!success && active) database_transaction_rollback(database);
g_free(timestamp); g_clear_pointer(&now, g_date_time_unref);
@ -200,100 +151,3 @@ cleanup:
evidence_entity_dao_free(link_dao); entity_dao_free(entity_dao);
return success;
}
static gboolean eml_integration_read_count(Database *database,
const char *sql, const char *identifier, gint64 *out_count)
{
DatabaseStatement *statement = database_statement_prepare(database, sql);
gboolean success = statement != NULL &&
database_statement_bind_text(statement, 1, identifier) &&
database_statement_step(statement) == DATABASE_STATEMENT_STEP_ROW &&
database_statement_column_int64(statement, 0, out_count);
database_statement_finalize(statement);
return success;
}
gboolean eml_integration_remove_promotion(Database *database,
const char *observation_identifier, gboolean *out_entity_deleted,
gboolean *out_entity_shared, GError **error)
{
static const char *read_sql =
"SELECT evidence_id,entity_id FROM evidence_entity_observations "
"WHERE id=?;";
static const char *detach_sql =
"UPDATE evidence_entity_observations SET entity_id=NULL,"
"promoted_at=NULL,promotion_kind=NULL WHERE id=? AND entity_id=?;";
static const char *dependency_sql =
"SELECT "
"(SELECT COUNT(*) FROM evidence_entity_observations WHERE entity_id=?1)+"
"(SELECT COUNT(*) FROM preuve_entites WHERE entite_id=?1)+"
"(SELECT COUNT(*) FROM preuve_entite_sources WHERE entite_id=?1)+"
"(SELECT COUNT(*) FROM relations WHERE entite_source_id=?1 OR entite_cible_id=?1)+"
"(SELECT COUNT(*) FROM tag_entites WHERE entite_id=?1)+"
"(SELECT COUNT(*) FROM recherche_entites WHERE entite_id=?1)+"
"(SELECT COUNT(*) FROM entite_chronologie WHERE entite_id=?1)+"
"(SELECT COUNT(*) FROM hypothese_entites WHERE entite_id=?1)+"
"(SELECT COUNT(*) FROM osint_execution_entities WHERE entity_id=?1)+"
"(SELECT COUNT(*) FROM comptes_sociaux WHERE entite_id=?1)+"
"(SELECT COUNT(*) FROM person_roles WHERE entity_id=?1);";
static const char *delete_sql = "DELETE FROM entites WHERE id=?;";
DatabaseStatement *statement = NULL;
EvidenceEntityDao *link_dao = NULL;
char *evidence_identifier = NULL, *entity_identifier = NULL;
gint64 count = 0; gboolean active = FALSE, success = FALSE;
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
if (out_entity_deleted != NULL) *out_entity_deleted = FALSE;
if (out_entity_shared != NULL) *out_entity_shared = FALSE;
if (database == NULL || !g_uuid_string_is_valid(observation_identifier))
return FALSE;
if (!database_transaction_begin(database)) return FALSE;
active = TRUE;
link_dao = evidence_entity_dao_new(database, error);
if (link_dao == NULL) goto cleanup;
statement = database_statement_prepare(database, read_sql);
if (statement == NULL ||
!database_statement_bind_text(statement, 1, observation_identifier) ||
database_statement_step(statement) != DATABASE_STATEMENT_STEP_ROW ||
!database_statement_column_text(statement, 0, &evidence_identifier) ||
!database_statement_column_text(statement, 1, &entity_identifier))
goto cleanup;
database_statement_finalize(statement); statement = NULL;
if (entity_identifier == NULL)
{
g_set_error_literal(error,
g_quark_from_static_string("eml-integration-error"), 4,
"Cette observation nest pas ajoutée au graphe.");
goto cleanup;
}
statement = database_statement_prepare(database, detach_sql);
if (statement == NULL ||
!database_statement_bind_text(statement, 1, observation_identifier) ||
!database_statement_bind_text(statement, 2, entity_identifier) ||
database_statement_step(statement) != DATABASE_STATEMENT_STEP_DONE)
goto cleanup;
database_statement_finalize(statement); statement = NULL;
if (!evidence_entity_dao_remove_source(link_dao, evidence_identifier,
entity_identifier, "eml_observation", observation_identifier,
NULL, error)) goto cleanup;
if (!eml_integration_read_count(database, dependency_sql,
entity_identifier, &count)) goto cleanup;
if (count == 0)
{
statement = database_statement_prepare(database, delete_sql);
if (statement == NULL ||
!database_statement_bind_text(statement, 1, entity_identifier) ||
database_statement_step(statement) != DATABASE_STATEMENT_STEP_DONE)
goto cleanup;
database_statement_finalize(statement); statement = NULL;
if (out_entity_deleted != NULL) *out_entity_deleted = TRUE;
}
else if (out_entity_shared != NULL) *out_entity_shared = TRUE;
if (!database_transaction_commit(database)) goto cleanup;
active = FALSE; success = TRUE;
cleanup:
database_statement_finalize(statement);
if (!success && active) database_transaction_rollback(database);
evidence_entity_dao_free(link_dao);
g_free(evidence_identifier); g_free(entity_identifier);
return success;
}

File diff suppressed because it is too large Load diff

View file

@ -4,6 +4,7 @@
******************************************************************************/
#include "core/eml_pipeline_task.h"
#include "core/file_hash.h"
#include "core/rib_ocr.h"
#include <gio/gio.h>
#include <glib.h>
#include <string.h>
@ -13,12 +14,6 @@ typedef struct
char *eml_path;
char *processed_evidence_dir;
char *evidence_id;
char *exiftool;
char *tesseract;
char *pdfinfo;
char *pdftotext;
char *pdftoppm;
guint document_analysis_limit;
} EmlPipelineTaskData;
static void eml_pipeline_task_data_free(gpointer user_data)
@ -29,11 +24,6 @@ static void eml_pipeline_task_data_free(gpointer user_data)
g_free(data->eml_path);
g_free(data->processed_evidence_dir);
g_free(data->evidence_id);
g_free(data->exiftool);
g_free(data->tesseract);
g_free(data->pdfinfo);
g_free(data->pdftotext);
g_free(data->pdftoppm);
g_free(data);
}
@ -47,8 +37,6 @@ void eml_pipeline_result_free(EmlPipelineResult *res)
eml_mime_result_free(res->mime_result);
if (res->bank_proposals != NULL)
g_ptr_array_unref(res->bank_proposals);
if (res->document_analyses != NULL)
g_ptr_array_unref(res->document_analyses);
if (res->warnings != NULL)
g_ptr_array_unref(res->warnings);
g_free(res);
@ -89,57 +77,23 @@ static gboolean eml_pipeline_task_worker(BackgroundTask *task,
return FALSE;
}
EmlMimeResult *mime_res = eml_mime_extract_attachments_cancellable(
data->eml_path,
target_dir,
cancellable,
error
);
EmlMimeResult *mime_res = eml_mime_extract_attachments(data->eml_path, target_dir, error);
g_free(target_dir);
if (mime_res == NULL)
{
if (g_cancellable_is_cancelled(cancellable))
{
eml_analysis_free(analysis);
return FALSE;
}
/* Si l'extraction MIME échoue, on conserve quand même l'analyse des en-têtes (résultat partiel) */
g_clear_error(error);
mime_res = g_new0(EmlMimeResult, 1);
mime_res->attachments = g_ptr_array_new_with_free_func((GDestroyNotify) eml_attachment_free);
mime_res->warnings = g_ptr_array_new_with_free_func(g_free);
g_ptr_array_add(mime_res->warnings, g_strdup("L'extraction MIME a échoué ou ne contient aucune pièce jointe."));
}
background_task_report_progress(task, 0.65,
"Analyse des pièces jointes avec ExifTool…");
background_task_report_progress(task, 0.75, "Analyse OCR et détection bancaire...");
GPtrArray *bank_proposals = g_ptr_array_new_with_free_func((GDestroyNotify) bank_proposal_free);
GPtrArray *document_analyses = g_ptr_array_new_with_free_func(
(GDestroyNotify) document_file_analysis_free);
GPtrArray *pipeline_warnings =
g_ptr_array_new_with_free_func(g_free);
guint skipped_document_analyses = 0;
for (guint i = 0; mime_res->attachments != NULL && i < mime_res->attachments->len; i++)
{
if (g_cancellable_is_cancelled(cancellable))
{
eml_analysis_free(analysis);
eml_mime_result_free(mime_res);
g_ptr_array_unref(bank_proposals);
g_ptr_array_unref(document_analyses);
g_ptr_array_unref(pipeline_warnings);
g_set_error_literal(
error,
G_IO_ERROR,
G_IO_ERROR_CANCELLED,
"L'analyse EML a été annulée."
);
return FALSE;
}
EmlAttachment *att = g_ptr_array_index(mime_res->attachments, i);
if (att->extracted_path == NULL)
continue;
@ -159,51 +113,13 @@ static gboolean eml_pipeline_task_worker(BackgroundTask *task,
g_free(content);
}
}
else if (ocr_analysis_mime_is_compatible(att->detected_mime) ||
g_strcmp0(att->detected_mime, "application/pdf") == 0 ||
g_strcmp0(att->content_type, "application/pdf") == 0)
else if (g_str_has_suffix(att->extracted_path, ".png") || g_str_has_suffix(att->extracted_path, ".jpg") || g_str_has_suffix(att->extracted_path, ".jpeg"))
{
background_task_report_progress(task, 0.78,
g_strcmp0(att->detected_mime, "application/pdf") == 0
? "Extraction du texte PDF ou OCR…"
: "Reconnaissance OCR de la pièce jointe…");
if (document_analyses->len >= data->document_analysis_limit)
{
skipped_document_analyses++;
continue;
}
DocumentAnalysisTools tools = {
.exiftool = data->exiftool,
.tesseract = data->tesseract,
.pdfinfo = data->pdfinfo,
.pdftotext = data->pdftotext,
.pdftoppm = data->pdftoppm
};
GError *analysis_error = NULL;
DocumentFileAnalysis *document =
document_file_analysis_run(&tools, att->extracted_path,
att->content_type, att->detected_mime, TRUE,
"fra+eng", cancellable, &analysis_error);
if (document == NULL &&
analysis_error != NULL &&
g_error_matches(analysis_error, G_IO_ERROR,
G_IO_ERROR_CANCELLED))
{
g_propagate_error(error, analysis_error);
eml_analysis_free(analysis);
eml_mime_result_free(mime_res);
g_ptr_array_unref(bank_proposals);
g_ptr_array_unref(document_analyses);
g_ptr_array_unref(pipeline_warnings);
return FALSE;
}
g_clear_error(&analysis_error);
if (document == NULL)
continue;
g_ptr_array_add(document_analyses, document);
const char *ocr_text = document->ocr != NULL
? document->ocr->text
: NULL;
char *ocr_text = NULL;
char *ocr_version = NULL;
(void) rib_ocr_extract_text(att->extracted_path, &ocr_text,
&ocr_version, NULL);
g_free(ocr_version);
if (ocr_text != NULL)
{
BankProposal *bp = bank_proposal_analyze_text(ocr_text, data->evidence_id);
@ -212,36 +128,22 @@ static gboolean eml_pipeline_task_worker(BackgroundTask *task,
bp->extraction_id = g_strdup(att->part_index);
g_ptr_array_add(bank_proposals, bp);
}
g_free(ocr_text);
}
}
}
background_task_report_progress(task, 0.92,
"Détection des données bancaires terminée.");
background_task_report_progress(task, 0.98,
"Préparation du dialogue de révision…");
background_task_report_progress(task, 1.0, "Analyse EML terminée avec succès.");
EmlPipelineResult *res = g_new0(EmlPipelineResult, 1);
res->analysis = analysis;
res->mime_result = mime_res;
res->bank_proposals = bank_proposals;
res->document_analyses = document_analyses;
res->warnings = pipeline_warnings;
res->skipped_document_analyses = skipped_document_analyses;
res->state = skipped_document_analyses > 0
? DOCUMENT_ANALYSIS_STATE_PARTIAL
: DOCUMENT_ANALYSIS_STATE_SUCCESS;
if (skipped_document_analyses > 0)
g_ptr_array_add(res->warnings, g_strdup_printf(
"La limite d'analyses documentaires est atteinte : "
"%u fichier(s) n'ont pas été analysés.",
skipped_document_analyses));
res->warnings = g_ptr_array_new_with_free_func(g_free);
if (out_result != NULL)
*out_result = res;
background_task_report_progress(task, 1.0,
"Analyse EML terminée avec succès.");
return TRUE;
}
@ -249,50 +151,13 @@ BackgroundTask *eml_pipeline_task_new(const char *eml_path,
const char *processed_evidence_dir,
const char *evidence_id)
{
DocumentAnalysisTools tools = {
.exiftool = "exiftool",
.tesseract = "tesseract",
.pdfinfo = "pdfinfo",
.pdftotext = "pdftotext",
.pdftoppm = "pdftoppm"
};
return eml_pipeline_task_new_with_tools(eml_path,
processed_evidence_dir, evidence_id, &tools);
}
BackgroundTask *eml_pipeline_task_new_with_tools(
const char *eml_path,
const char *processed_evidence_dir,
const char *evidence_id,
const DocumentAnalysisTools *tools)
{
return eml_pipeline_task_new_with_tools_and_limit(
eml_path, processed_evidence_dir, evidence_id, tools,
DOCUMENT_ANALYSIS_MAX_PIPELINE_ITEMS);
}
BackgroundTask *eml_pipeline_task_new_with_tools_and_limit(
const char *eml_path,
const char *processed_evidence_dir,
const char *evidence_id,
const DocumentAnalysisTools *tools,
guint document_analysis_limit)
{
if (eml_path == NULL || processed_evidence_dir == NULL || tools == NULL)
return NULL;
if (document_analysis_limit == 0)
if (eml_path == NULL || processed_evidence_dir == NULL)
return NULL;
EmlPipelineTaskData *data = g_new0(EmlPipelineTaskData, 1);
data->eml_path = g_strdup(eml_path);
data->processed_evidence_dir = g_strdup(processed_evidence_dir);
data->evidence_id = g_strdup(evidence_id);
data->exiftool = g_strdup(tools->exiftool);
data->tesseract = g_strdup(tools->tesseract);
data->pdfinfo = g_strdup(tools->pdfinfo);
data->pdftotext = g_strdup(tools->pdftotext);
data->pdftoppm = g_strdup(tools->pdftoppm);
data->document_analysis_limit = document_analysis_limit;
BackgroundTask *task = background_task_new(
"Analyse du message EML et de ses pièces jointes");
@ -310,43 +175,3 @@ BackgroundTask *eml_pipeline_task_new_with_tools_and_limit(
}
return task;
}
BackgroundTask *eml_pipeline_task_start(
const char *eml_path,
const char *staging_directory,
const char *evidence_id,
const DocumentAnalysisTools *tools,
BackgroundTaskCompletionCallback completion_callback,
gpointer completion_data,
GDestroyNotify completion_data_destroy)
{
if (eml_path == NULL || staging_directory == NULL || tools == NULL)
return NULL;
EmlPipelineTaskData *data = g_new0(EmlPipelineTaskData, 1);
data->eml_path = g_strdup(eml_path);
data->processed_evidence_dir = g_strdup(staging_directory);
data->evidence_id = g_strdup(evidence_id);
data->exiftool = g_strdup(tools->exiftool);
data->tesseract = g_strdup(tools->tesseract);
data->pdfinfo = g_strdup(tools->pdfinfo);
data->pdftotext = g_strdup(tools->pdftotext);
data->pdftoppm = g_strdup(tools->pdftoppm);
data->document_analysis_limit = DOCUMENT_ANALYSIS_MAX_PIPELINE_ITEMS;
BackgroundTask *task = background_task_new(
"Analyse complète de le-mail");
GError *error = NULL;
if (task == NULL || !background_task_start(task, eml_pipeline_task_worker,
data, eml_pipeline_task_data_free,
(GDestroyNotify) eml_pipeline_result_free,
completion_callback, completion_data, completion_data_destroy,
&error))
{
if (task != NULL)
background_task_unref(task);
else
eml_pipeline_task_data_free(data);
g_clear_error(&error);
return NULL;
}
return task;
}

View file

@ -1,279 +0,0 @@
/******************************************************************************
* @file exiftool_analysis.c
* @brief Analyse ExifTool structurée et traçable.
******************************************************************************/
#include "core/exiftool_analysis.h"
#include "core/document_tool_runner.h"
#include <string.h>
typedef struct
{
const char *tag;
const char *code;
gboolean sensitive;
} ExiftoolMapping;
static const ExiftoolMapping exiftool_mappings[] = {
{ "File:MIMEType", "file.mime_type", FALSE },
{ "File:FileSize", "file.size_bytes", FALSE },
{ "File:FileTypeExtension", "file.detected_extension", FALSE },
{ "EXIF:ImageWidth", "image.width", FALSE },
{ "EXIF:ImageHeight", "image.height", FALSE },
{ "EXIF:Orientation", "image.orientation", FALSE },
{ "EXIF:Software", "image.software", FALSE },
{ "EXIF:Make", "image.make", FALSE },
{ "EXIF:Model", "image.model", FALSE },
{ "EXIF:DateTimeOriginal", "image.datetime_original", FALSE },
{ "EXIF:GPSLatitude", "image.gps_latitude", TRUE },
{ "EXIF:GPSLongitude", "image.gps_longitude", TRUE },
{ "PDF:Author", "document.author", FALSE },
{ "PDF:Creator", "document.creator", FALSE },
{ "PDF:Producer", "document.producer", FALSE },
{ "PDF:CreateDate", "document.creation_time", FALSE },
{ "PDF:ModifyDate", "document.modification_time", FALSE }
};
static void exiftool_metadata_entry_free(gpointer data)
{
DocumentMetadataEntry *entry = data;
if (entry == NULL)
return;
g_free(entry->code);
g_free(entry->original_group);
g_free(entry->original_tag);
g_free(entry->raw_value);
g_free(entry);
}
void exiftool_analysis_result_free(ExiftoolAnalysisResult *result)
{
if (result == NULL)
return;
document_tool_execution_free(result->execution);
g_ptr_array_unref(result->metadata);
g_free(result);
}
static char *exiftool_json_extract_value(
const char *json,
const char *tag
)
{
char *escaped = g_regex_escape_string(tag, -1);
char *pattern = g_strdup_printf(
"\"%s\"\\s*:\\s*(\"(?:[^\"\\\\]|\\\\.)*\"|-?[0-9]+(?:\\.[0-9]+)?|true|false|null)",
escaped
);
GRegex *regex = g_regex_new(pattern, G_REGEX_DOTALL, 0, NULL);
GMatchInfo *match = NULL;
char *value = NULL;
g_regex_match(regex, json, 0, &match);
if (g_match_info_matches(match))
{
value = g_match_info_fetch(match, 1);
if (value[0] == '"' && strlen(value) >= 2)
{
gsize length = strlen(value);
memmove(value, value + 1, length - 2);
value[length - 2] = '\0';
}
}
g_match_info_free(match);
g_regex_unref(regex);
g_free(pattern);
g_free(escaped);
return value;
}
static gboolean exiftool_json_shape_is_valid(const char *json)
{
char *copy = json != NULL ? g_strdup(json) : NULL;
gboolean valid = FALSE;
if (copy != NULL)
{
g_strstrip(copy);
gsize length = strlen(copy);
valid = length >= 2 && copy[0] == '[' && copy[length - 1] == ']';
}
g_free(copy);
return valid;
}
static gboolean exiftool_metadata_contains_tag(
const GPtrArray *metadata,
const char *group,
const char *tag
)
{
for (guint index = 0; index < metadata->len; index++)
{
const DocumentMetadataEntry *entry =
g_ptr_array_index((GPtrArray *) metadata, index);
if (g_strcmp0(entry->original_group, group) == 0 &&
g_strcmp0(entry->original_tag, tag) == 0)
return TRUE;
}
return FALSE;
}
static void exiftool_analysis_add_unknown_tags(
ExiftoolAnalysisResult *result,
const char *json
)
{
GRegex *regex = g_regex_new(
"\"([A-Za-z0-9_ -]+):([A-Za-z0-9_ -]+)\"\\s*:\\s*"
"(\"(?:[^\"\\\\]|\\\\.)*\"|-?[0-9]+(?:\\.[0-9]+)?|true|false|null)",
G_REGEX_DOTALL, 0, NULL);
GMatchInfo *match = NULL;
g_regex_match(regex, json, 0, &match);
while (g_match_info_matches(match))
{
char *group = g_match_info_fetch(match, 1);
char *tag = g_match_info_fetch(match, 2);
char *value = g_match_info_fetch(match, 3);
if (!exiftool_metadata_contains_tag(result->metadata, group, tag))
{
DocumentMetadataEntry *entry =
g_new0(DocumentMetadataEntry, 1);
entry->code = g_strdup("metadata.unknown");
entry->original_group = group;
entry->original_tag = tag;
entry->raw_value = value;
g_ptr_array_add(result->metadata, entry);
}
else
{
g_free(group);
g_free(tag);
g_free(value);
}
if (!g_match_info_next(match, NULL))
break;
}
g_match_info_free(match);
g_regex_unref(regex);
}
ExiftoolAnalysisResult *exiftool_analysis_parse(
const char *file_path,
const char *json,
const char *stderr_text,
int exit_status,
GError **error
)
{
if (file_path == NULL || json == NULL)
{
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
"Le résultat ExifTool à analyser est invalide.");
return NULL;
}
if (!exiftool_json_shape_is_valid(json))
{
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
"La sortie JSON ExifTool est invalide ou tronquée.");
return NULL;
}
ExiftoolAnalysisResult *result = g_new0(ExiftoolAnalysisResult, 1);
result->execution = document_tool_execution_new("exiftool", file_path);
result->metadata = g_ptr_array_new_with_free_func(
exiftool_metadata_entry_free);
result->execution->raw_stdout = g_strdup(json);
result->execution->raw_stdout_sha256 = g_compute_checksum_for_string(
G_CHECKSUM_SHA256, json, -1);
result->execution->raw_stderr = g_strdup(stderr_text);
result->execution->exit_status = exit_status;
result->execution->state = exit_status == 0
? DOCUMENT_ANALYSIS_STATE_SUCCESS
: DOCUMENT_ANALYSIS_STATE_PARTIAL;
for (guint index = 0; index < G_N_ELEMENTS(exiftool_mappings); index++)
{
char *value = exiftool_json_extract_value(
json, exiftool_mappings[index].tag);
if (value == NULL)
continue;
DocumentMetadataEntry *entry = g_new0(DocumentMetadataEntry, 1);
entry->code = g_strdup(exiftool_mappings[index].code);
const char *colon = strchr(exiftool_mappings[index].tag, ':');
entry->original_group = g_strndup(exiftool_mappings[index].tag,
(gsize) (colon - exiftool_mappings[index].tag));
entry->original_tag = g_strdup(colon + 1);
entry->raw_value = value;
entry->sensitive = exiftool_mappings[index].sensitive;
entry->requires_confirmation = entry->sensitive;
g_ptr_array_add(result->metadata, entry);
}
exiftool_analysis_add_unknown_tags(result, json);
return result;
}
ExiftoolAnalysisResult *exiftool_analysis_run(
const char *executable,
const char *file_path,
GCancellable *cancellable,
GError **error
)
{
const DocumentToolRunnerLimits limits = {
DOCUMENT_ANALYSIS_MAX_STDOUT,
DOCUMENT_ANALYSIS_MAX_STDERR
};
return exiftool_analysis_run_with_limits(
executable, file_path, &limits, cancellable, error);
}
ExiftoolAnalysisResult *exiftool_analysis_run_with_limits(
const char *executable,
const char *file_path,
const DocumentToolRunnerLimits *limits,
GCancellable *cancellable,
GError **error
)
{
const char *arguments[] = { "-j", "-G1", "-n", "--", file_path, NULL };
const char *version_arguments[] = { "-ver", NULL };
DocumentToolExecution *execution = NULL;
if (!document_tool_runner_run_with_limits("exiftool", executable,
arguments, file_path, limits, cancellable, &execution, error))
{
document_tool_execution_free(execution);
return NULL;
}
if (execution->state == DOCUMENT_ANALYSIS_STATE_UNAVAILABLE)
{
ExiftoolAnalysisResult *unavailable =
g_new0(ExiftoolAnalysisResult, 1);
unavailable->execution = execution;
unavailable->metadata = g_ptr_array_new_with_free_func(
exiftool_metadata_entry_free);
return unavailable;
}
execution->version = document_tool_runner_read_version(
executable, version_arguments, cancellable);
if (execution->stdout_truncated)
{
ExiftoolAnalysisResult *truncated =
g_new0(ExiftoolAnalysisResult, 1);
truncated->execution = execution;
truncated->metadata = g_ptr_array_new_with_free_func(
exiftool_metadata_entry_free);
execution->state = DOCUMENT_ANALYSIS_STATE_FAILED;
g_ptr_array_add(execution->errors, g_strdup(
"Le JSON ExifTool tronqué n'a pas été interprété."));
return truncated;
}
ExiftoolAnalysisResult *result = exiftool_analysis_parse(file_path,
execution->raw_stdout != NULL ? execution->raw_stdout : "",
execution->raw_stderr, execution->exit_status, error);
if (result != NULL)
{
document_tool_execution_free(result->execution);
result->execution = execution;
}
else
document_tool_execution_free(execution);
return result;
}

View file

@ -237,6 +237,7 @@ static gboolean extraction_drop_service_attach_in_transaction(
{
EvidenceEntityDao *link_dao = NULL;
char *evidence_identifier = NULL;
gboolean exists = FALSE;
gboolean success = FALSE;
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
@ -253,8 +254,10 @@ static gboolean extraction_drop_service_attach_in_transaction(
if (evidence_identifier == NULL) goto cleanup;
link_dao = evidence_entity_dao_new(database, error);
if (link_dao == NULL ||
!evidence_entity_dao_link(link_dao, evidence_identifier,
entity_identifier, error)) goto cleanup;
!evidence_entity_dao_exists(link_dao, evidence_identifier,
entity_identifier, &exists, error) ||
(!exists && !evidence_entity_dao_link(link_dao, evidence_identifier,
entity_identifier, error))) goto cleanup;
success = TRUE;
cleanup:
evidence_entity_dao_free(link_dao);

View file

@ -1,98 +0,0 @@
/******************************************************************************
* @file ocr_analysis.c
* @brief OCR Tesseract traçable et annulable.
******************************************************************************/
#include "core/ocr_analysis.h"
#include "core/document_tool_runner.h"
gboolean ocr_analysis_mime_is_compatible(const char *mime_type)
{
return g_strcmp0(mime_type, "image/png") == 0 ||
g_strcmp0(mime_type, "image/jpeg") == 0 ||
g_strcmp0(mime_type, "image/tiff") == 0;
}
void ocr_analysis_result_free(OcrAnalysisResult *result)
{
if (result == NULL)
return;
document_tool_execution_free(result->execution);
g_free(result->requested_languages);
g_free(result->text);
g_free(result);
}
OcrAnalysisResult *ocr_analysis_run(
const char *executable,
const char *image_path,
const char *languages,
GCancellable *cancellable,
GError **error
)
{
const DocumentToolRunnerLimits limits = {
DOCUMENT_ANALYSIS_MAX_TEXT,
DOCUMENT_ANALYSIS_MAX_STDERR
};
return ocr_analysis_run_with_limits(executable, image_path, languages,
&limits, cancellable, error);
}
OcrAnalysisResult *ocr_analysis_run_with_limits(
const char *executable,
const char *image_path,
const char *languages,
const DocumentToolRunnerLimits *limits,
GCancellable *cancellable,
GError **error
)
{
if (executable == NULL || image_path == NULL || languages == NULL ||
languages[0] == '\0' ||
(!g_str_equal(languages, "fra") &&
!g_str_equal(languages, "eng") &&
!g_str_equal(languages, "fra+eng")))
{
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
"Les paramètres OCR sont invalides.");
return NULL;
}
const char *arguments[] = {
image_path, "stdout", "-l", languages, NULL
};
const char *version_arguments[] = { "--version", NULL };
DocumentToolExecution *execution = NULL;
if (!document_tool_runner_run_with_limits("tesseract", executable,
arguments, image_path, limits, cancellable, &execution, error))
{
document_tool_execution_free(execution);
return NULL;
}
OcrAnalysisResult *result = g_new0(OcrAnalysisResult, 1);
result->execution = execution;
execution->version = document_tool_runner_read_version(
executable, version_arguments, cancellable);
result->requested_languages = g_strdup(languages);
if (execution->raw_stdout != NULL)
{
gsize length = strlen(execution->raw_stdout);
if (length > DOCUMENT_ANALYSIS_MAX_TEXT)
{
result->text = g_strndup(execution->raw_stdout,
DOCUMENT_ANALYSIS_MAX_TEXT);
execution->state = DOCUMENT_ANALYSIS_STATE_PARTIAL;
g_ptr_array_add(execution->warnings,
g_strdup("Le texte OCR dépasse la limite autorisée."));
}
else
result->text = g_strdup(execution->raw_stdout);
}
if (execution->state == DOCUMENT_ANALYSIS_STATE_SUCCESS &&
(result->text == NULL || result->text[0] == '\0'))
{
execution->state = DOCUMENT_ANALYSIS_STATE_PARTIAL;
g_ptr_array_add(execution->warnings,
g_strdup("Tesseract n'a produit aucun texte."));
}
return result;
}

View file

@ -1,327 +0,0 @@
/******************************************************************************
* @file pdf_analysis.c
* @brief Extraction PDF native puis OCR de secours.
******************************************************************************/
#include "core/pdf_analysis.h"
#include "core/document_tool_runner.h"
#include <glib/gstdio.h>
static void pdf_page_analysis_free(gpointer data)
{
PdfPageAnalysis *page = data;
if (page == NULL)
return;
g_free(page->text);
document_tool_execution_free(page->render_execution);
document_tool_execution_free(page->execution);
g_ptr_array_unref(page->warnings);
g_free(page);
}
void pdf_analysis_result_free(PdfAnalysisResult *result)
{
if (result == NULL)
return;
g_free(result->source_path);
g_free(result->native_text);
document_tool_execution_free(result->pdfinfo_execution);
document_tool_execution_free(result->native_execution);
g_ptr_array_unref(result->pages);
g_ptr_array_unref(result->warnings);
g_free(result);
}
gboolean pdf_analysis_text_is_usable(const char *text)
{
if (text == NULL)
return FALSE;
gsize total = 0;
gsize non_space = 0;
gsize printable = 0;
for (const char *cursor = text; *cursor != '\0';
cursor = g_utf8_next_char(cursor))
{
gunichar character = g_utf8_get_char(cursor);
total++;
if (!g_unichar_isspace(character))
non_space++;
if (g_unichar_isprint(character) || g_unichar_isspace(character))
printable++;
}
return non_space >= 32 && total > 0 &&
((double) printable / (double) total) >= 0.70;
}
static guint pdf_analysis_parse_pages(const char *text)
{
GRegex *regex = g_regex_new("(?im)^Pages:\\s*([0-9]+)", 0, 0, NULL);
GMatchInfo *match = NULL;
guint pages = 0;
g_regex_match(regex, text != NULL ? text : "", 0, &match);
if (g_match_info_matches(match))
{
char *value = g_match_info_fetch(match, 1);
pages = (guint) g_ascii_strtoull(value, NULL, 10);
g_free(value);
}
g_match_info_free(match);
g_regex_unref(regex);
return pages;
}
static gboolean pdf_analysis_parse_encrypted(const char *text)
{
GRegex *regex = g_regex_new(
"(?im)^Encrypted:\\s*(yes|oui|true)", 0, 0, NULL);
gboolean encrypted = g_regex_match(
regex, text != NULL ? text : "", 0, NULL);
g_regex_unref(regex);
return encrypted;
}
static PdfPageAnalysis *pdf_page_new(
guint page_number,
PdfPageMethod method,
const char *text,
DocumentAnalysisState state
)
{
PdfPageAnalysis *page = g_new0(PdfPageAnalysis, 1);
page->page_number = page_number;
page->method = method;
page->text = g_strdup(text);
page->state = state;
page->warnings = g_ptr_array_new_with_free_func(g_free);
return page;
}
static void pdf_analysis_add_native_pages(PdfAnalysisResult *result)
{
char **pages = g_strsplit(result->native_text, "\f", -1);
guint added = 0;
for (guint index = 0; pages[index] != NULL; index++)
{
if (pages[index][0] == '\0' && pages[index + 1] == NULL)
break;
g_ptr_array_add(result->pages, pdf_page_new(
index + 1, PDF_PAGE_METHOD_NATIVE, pages[index],
DOCUMENT_ANALYSIS_STATE_SUCCESS));
added++;
}
if (result->page_count == 0)
result->page_count = added;
g_strfreev(pages);
}
static gboolean pdf_analysis_render_and_ocr(
PdfAnalysisResult *result,
const PdfAnalysisTools *tools,
const char *languages,
GCancellable *cancellable,
GError **error
)
{
GError *temporary_error = NULL;
char *temporary_directory = g_dir_make_tmp(
"labfy-pdf-analysis-XXXXXX", &temporary_error);
if (temporary_directory == NULL)
{
g_propagate_error(error, temporary_error);
return FALSE;
}
guint pages = MIN(result->page_count, DOCUMENT_ANALYSIS_MAX_PDF_PAGES);
if (result->page_count > DOCUMENT_ANALYSIS_MAX_PDF_PAGES)
{
result->state = DOCUMENT_ANALYSIS_STATE_PARTIAL;
g_ptr_array_add(result->warnings,
g_strdup("Le nombre de pages PDF dépasse la limite."));
}
gboolean success = TRUE;
char *source_basename = g_path_get_basename(result->source_path);
for (guint page_number = 1; page_number <= pages; page_number++)
{
if (cancellable != NULL &&
g_cancellable_set_error_if_cancelled(cancellable, error))
{
result->state = result->pages->len > 0
? DOCUMENT_ANALYSIS_STATE_PARTIAL
: DOCUMENT_ANALYSIS_STATE_CANCELLED;
success = FALSE;
break;
}
char *prefix = g_strdup_printf("%s/%s-page-%u",
temporary_directory, source_basename, page_number);
char *page_text = g_strdup_printf("%u", page_number);
const char *render_arguments[] = {
"-f", page_text, "-singlefile", "-png",
result->source_path, prefix, NULL
};
DocumentToolExecution *render_execution = NULL;
if (!document_tool_runner_run("pdftoppm", tools->pdftoppm,
render_arguments, result->source_path, cancellable,
&render_execution, error))
{
result->state = result->pages->len > 0
? DOCUMENT_ANALYSIS_STATE_PARTIAL
: DOCUMENT_ANALYSIS_STATE_CANCELLED;
document_tool_execution_free(render_execution);
g_free(page_text);
g_free(prefix);
success = FALSE;
break;
}
char *image_path = g_strconcat(prefix, ".png", NULL);
if (render_execution->state != DOCUMENT_ANALYSIS_STATE_SUCCESS)
{
PdfPageAnalysis *page = pdf_page_new(page_number,
PDF_PAGE_METHOD_OCR, NULL,
DOCUMENT_ANALYSIS_STATE_FAILED);
page->execution = render_execution;
g_ptr_array_add(result->pages, page);
result->state = DOCUMENT_ANALYSIS_STATE_PARTIAL;
}
else
{
OcrAnalysisResult *ocr = ocr_analysis_run(
tools->tesseract, image_path, languages,
cancellable, error);
PdfPageAnalysis *page = pdf_page_new(page_number,
PDF_PAGE_METHOD_OCR,
ocr != NULL ? ocr->text : NULL,
ocr != NULL ? ocr->execution->state :
DOCUMENT_ANALYSIS_STATE_FAILED);
page->render_execution = render_execution;
if (ocr != NULL)
{
page->execution = ocr->execution;
ocr->execution = NULL;
ocr_analysis_result_free(ocr);
}
g_ptr_array_add(result->pages, page);
if (page->state != DOCUMENT_ANALYSIS_STATE_SUCCESS)
result->state = DOCUMENT_ANALYSIS_STATE_PARTIAL;
if (ocr == NULL && error != NULL && *error != NULL &&
g_error_matches(*error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
{
result->state = result->pages->len > 1
? DOCUMENT_ANALYSIS_STATE_PARTIAL
: DOCUMENT_ANALYSIS_STATE_CANCELLED;
success = FALSE;
}
}
g_remove(image_path);
g_free(image_path);
g_free(page_text);
g_free(prefix);
if (!success)
break;
}
g_free(source_basename);
g_rmdir(temporary_directory);
g_free(temporary_directory);
return success;
}
PdfAnalysisResult *pdf_analysis_run(
const PdfAnalysisTools *tools,
const char *pdf_path,
const char *ocr_languages,
GCancellable *cancellable,
GError **error
)
{
if (tools == NULL || pdf_path == NULL || ocr_languages == NULL)
{
g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
"Les paramètres d'analyse PDF sont invalides.");
return NULL;
}
PdfAnalysisResult *result = g_new0(PdfAnalysisResult, 1);
result->source_path = g_strdup(pdf_path);
result->pages = g_ptr_array_new_with_free_func(pdf_page_analysis_free);
result->warnings = g_ptr_array_new_with_free_func(g_free);
result->state = DOCUMENT_ANALYSIS_STATE_SUCCESS;
const char *info_arguments[] = { pdf_path, NULL };
if (!document_tool_runner_run("pdfinfo", tools->pdfinfo,
info_arguments, pdf_path, cancellable,
&result->pdfinfo_execution, error))
{
if (error != NULL && *error != NULL &&
g_error_matches(*error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
{
result->state = DOCUMENT_ANALYSIS_STATE_CANCELLED;
g_clear_error(error);
return result;
}
goto failure;
}
if (result->pdfinfo_execution->state ==
DOCUMENT_ANALYSIS_STATE_UNAVAILABLE)
{
result->state = DOCUMENT_ANALYSIS_STATE_UNAVAILABLE;
return result;
}
const char *version_arguments[] = { "-v", NULL };
result->pdfinfo_execution->version =
document_tool_runner_read_version(
tools->pdfinfo, version_arguments, cancellable);
result->encrypted = pdf_analysis_parse_encrypted(
result->pdfinfo_execution->raw_stdout);
result->page_count = pdf_analysis_parse_pages(
result->pdfinfo_execution->raw_stdout);
if (result->encrypted)
{
result->state = DOCUMENT_ANALYSIS_STATE_PARTIAL;
g_ptr_array_add(result->warnings,
g_strdup("Le PDF est chiffré ; aucun contournement n'est tenté."));
return result;
}
const char *text_arguments[] = {
"-enc", "UTF-8", "-layout", pdf_path, "-", NULL
};
if (!document_tool_runner_run("pdftotext", tools->pdftotext,
text_arguments, pdf_path, cancellable,
&result->native_execution, error))
{
if (error != NULL && *error != NULL &&
g_error_matches(*error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
{
result->state = DOCUMENT_ANALYSIS_STATE_CANCELLED;
g_clear_error(error);
return result;
}
goto failure;
}
result->native_execution->version =
document_tool_runner_read_version(
tools->pdftotext, version_arguments, cancellable);
if (result->native_execution->state ==
DOCUMENT_ANALYSIS_STATE_SUCCESS)
result->native_text = g_strdup(
result->native_execution->raw_stdout);
result->native_text_usable = pdf_analysis_text_is_usable(
result->native_text);
if (result->native_text_usable)
pdf_analysis_add_native_pages(result);
else if (!pdf_analysis_render_and_ocr(result, tools, ocr_languages,
cancellable, error))
{
if (error != NULL && *error != NULL &&
g_error_matches(*error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
{
g_clear_error(error);
return result;
}
result->state = DOCUMENT_ANALYSIS_STATE_PARTIAL;
g_clear_error(error);
}
return result;
failure:
pdf_analysis_result_free(result);
return NULL;
}

View file

@ -52,7 +52,7 @@ static const char *const evidence_entity_dao_association_exists_sql =
* @brief Requête créant une association.
*/
static const char *const evidence_entity_dao_link_sql =
"INSERT OR IGNORE INTO preuve_entites"
"INSERT INTO preuve_entites"
"("
" preuve_id,"
" entite_id"
@ -71,36 +71,6 @@ static const char *const evidence_entity_dao_unlink_sql =
"WHERE preuve_id = ? "
"AND entite_id = ?;";
static void evidence_entity_dao_set_database_error(
EvidenceEntityDao *evidence_entity_dao, GError **error,
EvidenceEntityDaoError error_code, const char *context);
static gboolean evidence_entity_dao_execute_source_statement(
EvidenceEntityDao *dao, const char *sql, const char *evidence_identifier,
const char *entity_identifier, const char *source_kind,
const char *source_uuid, const char *created_at, GError **error)
{
DatabaseStatement *statement = database_statement_prepare(dao->database, sql);
gboolean success = statement != NULL &&
database_statement_bind_text(statement, 1, evidence_identifier) &&
database_statement_bind_text(statement, 2, entity_identifier) &&
database_statement_bind_text(statement, 3, source_kind);
if (success && source_uuid != NULL)
success = database_statement_bind_text(statement, 4, source_uuid);
if (success && created_at != NULL)
success = database_statement_bind_text(statement, 5, created_at);
if (success)
success = database_statement_step(statement) ==
DATABASE_STATEMENT_STEP_DONE;
if (!success)
evidence_entity_dao_set_database_error(dao, error,
statement == NULL ? EVIDENCE_ENTITY_DAO_ERROR_PREPARE :
EVIDENCE_ENTITY_DAO_ERROR_EXECUTE,
"Impossible de modifier la provenance de l'association");
database_statement_finalize(statement);
return success;
}
/**
* @brief Requête listant les entités liées à une preuve.
*/
@ -831,9 +801,8 @@ gboolean evidence_entity_dao_link(
gboolean entity_exists =
FALSE;
GDateTime *now = NULL;
char *created_at = NULL;
gboolean success = FALSE;
gboolean association_exists =
FALSE;
g_return_val_if_fail(
error == NULL || *error == NULL,
@ -896,20 +865,39 @@ gboolean evidence_entity_dao_link(
return FALSE;
}
now = g_date_time_new_now_utc();
created_at = now != NULL
? g_date_time_format(now, "%Y-%m-%dT%H:%M:%SZ") : NULL;
if (created_at == NULL)
evidence_entity_dao_set_error_literal(error,
EVIDENCE_ENTITY_DAO_ERROR_MEMORY,
"Impossible de dater l'association preuve-entité.");
else
success = evidence_entity_dao_add_source(evidence_entity_dao,
evidence_identifier, entity_identifier, "manual", NULL,
created_at, error);
g_free(created_at);
g_clear_pointer(&now, g_date_time_unref);
return success;
if (!evidence_entity_dao_query_exists(
evidence_entity_dao,
evidence_entity_dao_association_exists_sql,
evidence_identifier,
entity_identifier,
&association_exists,
error
))
{
return FALSE;
}
if (association_exists)
{
evidence_entity_dao_set_error_literal(
error,
EVIDENCE_ENTITY_DAO_ERROR_CONSTRAINT,
"Cette preuve est déjà associée à cette entité."
);
return FALSE;
}
return evidence_entity_dao_execute_pair_statement(
evidence_entity_dao,
evidence_entity_dao_link_sql,
evidence_identifier,
entity_identifier,
"Impossible de préparer la création de l'association",
"Impossible de lier les identifiants de l'association",
"Impossible de créer l'association",
error
);
}
gboolean evidence_entity_dao_unlink(
@ -960,103 +948,16 @@ gboolean evidence_entity_dao_unlink(
return FALSE;
}
return evidence_entity_dao_remove_source(evidence_entity_dao,
evidence_identifier, entity_identifier, "manual", NULL, NULL, error);
}
gboolean evidence_entity_dao_add_source(
EvidenceEntityDao *dao, const char *evidence_identifier,
const char *entity_identifier, const char *source_kind,
const char *source_uuid, const char *created_at, GError **error)
{
static const char *insert_sql =
"INSERT OR IGNORE INTO preuve_entite_sources"
"(id,preuve_id,entite_id,source_kind,source_uuid,created_at)"
"VALUES(?,?,?,?,?,?);";
char *source_identifier = NULL;
DatabaseStatement *statement = NULL;
gboolean success = FALSE;
if (!evidence_entity_dao_validate_association(dao, evidence_identifier,
entity_identifier, error) ||
source_kind == NULL || created_at == NULL ||
(g_strcmp0(source_kind, "eml_observation") == 0 &&
(source_uuid == NULL || !g_uuid_string_is_valid(source_uuid))))
return FALSE;
if (!evidence_entity_dao_execute_pair_statement(dao,
evidence_entity_dao_link_sql, evidence_identifier,
entity_identifier, "Impossible de préparer l'association",
"Impossible de lier l'association",
"Impossible de matérialiser l'association", error))
return FALSE;
source_identifier = g_uuid_string_random();
statement = database_statement_prepare(dao->database, insert_sql);
success = statement != NULL &&
database_statement_bind_text(statement, 1, source_identifier) &&
database_statement_bind_text(statement, 2, evidence_identifier) &&
database_statement_bind_text(statement, 3, entity_identifier) &&
database_statement_bind_text(statement, 4, source_kind);
if (success && source_uuid != NULL)
success = database_statement_bind_text(statement, 5, source_uuid);
if (success)
success = database_statement_bind_text(statement, 6, created_at) &&
database_statement_step(statement) == DATABASE_STATEMENT_STEP_DONE;
if (!success)
evidence_entity_dao_set_database_error(dao, error,
statement == NULL ? EVIDENCE_ENTITY_DAO_ERROR_PREPARE :
EVIDENCE_ENTITY_DAO_ERROR_EXECUTE,
"Impossible d'enregistrer la provenance de l'association");
database_statement_finalize(statement);
g_free(source_identifier);
return success;
}
gboolean evidence_entity_dao_remove_source(
EvidenceEntityDao *dao, const char *evidence_identifier,
const char *entity_identifier, const char *source_kind,
const char *source_uuid, gboolean *out_link_removed, GError **error)
{
static const char *delete_source_sql =
"DELETE FROM preuve_entite_sources WHERE preuve_id=?1 AND entite_id=?2 "
"AND source_kind=?3 AND COALESCE(source_uuid,'')=COALESCE(?4,'');";
static const char *count_sql =
"SELECT COUNT(*) FROM preuve_entite_sources "
"WHERE preuve_id=?1 AND entite_id=?2;";
DatabaseStatement *statement = NULL;
gint64 count = 0;
gboolean success = FALSE;
if (out_link_removed != NULL) *out_link_removed = FALSE;
if (!evidence_entity_dao_validate_association(dao, evidence_identifier,
entity_identifier, error) || source_kind == NULL)
return FALSE;
if (!evidence_entity_dao_execute_source_statement(dao, delete_source_sql,
evidence_identifier, entity_identifier, source_kind, source_uuid,
NULL, error))
return FALSE;
statement = database_statement_prepare(dao->database, count_sql);
success = statement != NULL &&
database_statement_bind_text(statement, 1, evidence_identifier) &&
database_statement_bind_text(statement, 2, entity_identifier) &&
database_statement_step(statement) == DATABASE_STATEMENT_STEP_ROW &&
database_statement_column_int64(statement, 0, &count);
database_statement_finalize(statement);
if (!success)
{
evidence_entity_dao_set_database_error(dao, error,
EVIDENCE_ENTITY_DAO_ERROR_EXECUTE,
"Impossible de vérifier les provenances de l'association");
return FALSE;
}
if (count == 0)
{
success = evidence_entity_dao_execute_pair_statement(dao,
evidence_entity_dao_unlink_sql, evidence_identifier,
entity_identifier, "Impossible de préparer le détachement",
"Impossible de lier le détachement",
"Impossible de supprimer l'association sans provenance", error);
if (success && out_link_removed != NULL) *out_link_removed = TRUE;
return success;
}
return TRUE;
return evidence_entity_dao_execute_pair_statement(
evidence_entity_dao,
evidence_entity_dao_unlink_sql,
evidence_identifier,
entity_identifier,
"Impossible de préparer la suppression de l'association",
"Impossible de lier les identifiants de l'association",
"Impossible de supprimer l'association",
error
);
}
gboolean evidence_entity_dao_exists(
@ -1166,218 +1067,3 @@ GPtrArray *evidence_entity_dao_list_evidence_identifiers(
error
);
}
gboolean evidence_entity_dao_add_observation(EvidenceEntityDao *dao,
const char *evidence_identifier, const char *entity_type,
const char *value_raw,
const char *value_normalized, const char *role,
const char *provenance_kind, const char *source_header,
guint occurrence, const char *verification_status,
const char *created_at, char **out_observation_identifier, GError **error)
{
static const char *sql =
"INSERT OR IGNORE INTO evidence_entity_observations("
"id,evidence_id,entity_type,value_raw,value_normalized,role,"
"provenance_kind,source_header,occurrence,verification_status,"
"observed_at,integrated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?);";
static const char *find_sql =
"SELECT id FROM evidence_entity_observations WHERE evidence_id=? "
"AND entity_type=? AND value_normalized=? AND role=? "
"AND source_header=? AND occurrence=? AND provenance_kind=? "
"AND extraction_id IS NULL LIMIT 1;";
DatabaseStatement *statement = NULL;
char *new_identifier = NULL;
gboolean success = FALSE;
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
if (out_observation_identifier != NULL) *out_observation_identifier = NULL;
if (!evidence_entity_dao_validate_list_request(dao, evidence_identifier,
"L'identifiant de preuve est invalide.", error) ||
entity_type == NULL ||
value_raw == NULL || value_normalized == NULL || role == NULL ||
source_header == NULL || occurrence == 0 || created_at == NULL)
return FALSE;
new_identifier = g_uuid_string_random();
statement = database_statement_prepare(dao->database, sql);
if (statement == NULL) goto cleanup;
success =
database_statement_bind_text(statement, 1, new_identifier) &&
database_statement_bind_text(statement, 2, evidence_identifier) &&
database_statement_bind_text(statement, 3, entity_type) &&
database_statement_bind_text(statement, 4, value_raw) &&
database_statement_bind_text(statement, 5, value_normalized) &&
database_statement_bind_text(statement, 6, role) &&
database_statement_bind_text(statement, 7, provenance_kind) &&
database_statement_bind_text(statement, 8, source_header) &&
database_statement_bind_int64(statement, 9, occurrence) &&
database_statement_bind_text(statement, 10, verification_status) &&
database_statement_bind_text(statement, 11, created_at) &&
database_statement_bind_text(statement, 12, created_at) &&
database_statement_step(statement) == DATABASE_STATEMENT_STEP_DONE;
database_statement_finalize(statement); statement = NULL;
if (!success) goto cleanup;
statement = database_statement_prepare(dao->database, find_sql);
success = statement != NULL &&
database_statement_bind_text(statement, 1, evidence_identifier) &&
database_statement_bind_text(statement, 2, entity_type) &&
database_statement_bind_text(statement, 3, value_normalized) &&
database_statement_bind_text(statement, 4, role) &&
database_statement_bind_text(statement, 5, source_header) &&
database_statement_bind_int64(statement, 6, occurrence) &&
database_statement_bind_text(statement, 7, provenance_kind) &&
database_statement_step(statement) == DATABASE_STATEMENT_STEP_ROW &&
database_statement_column_text(statement, 0,
out_observation_identifier);
cleanup:
if (!success)
evidence_entity_dao_set_database_error(dao, error,
EVIDENCE_ENTITY_DAO_ERROR_EXECUTE,
"Impossible denregistrer lobservation preuve-entité");
database_statement_finalize(statement);
g_free(new_identifier);
return success;
}
gboolean evidence_entity_dao_promote_observation(EvidenceEntityDao *dao,
const char *observation_identifier, const char *entity_identifier,
const char *promoted_at, const char *promotion_kind, GError **error)
{
static const char *sql =
"UPDATE evidence_entity_observations SET entity_id=?,promoted_at=?,"
"promotion_kind=? WHERE id=? AND (entity_id IS NULL OR entity_id=?);";
DatabaseStatement *statement = NULL;
gboolean success = FALSE;
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
if (dao == NULL || !g_uuid_string_is_valid(observation_identifier) ||
!g_uuid_string_is_valid(entity_identifier) || promoted_at == NULL ||
(g_strcmp0(promotion_kind, "created") != 0 &&
g_strcmp0(promotion_kind, "reused") != 0)) return FALSE;
statement = database_statement_prepare(dao->database, sql);
success = statement != NULL &&
database_statement_bind_text(statement, 1, entity_identifier) &&
database_statement_bind_text(statement, 2, promoted_at) &&
database_statement_bind_text(statement, 3, promotion_kind) &&
database_statement_bind_text(statement, 4, observation_identifier) &&
database_statement_bind_text(statement, 5, entity_identifier) &&
database_statement_step(statement) == DATABASE_STATEMENT_STEP_DONE;
if (!success)
evidence_entity_dao_set_database_error(dao, error,
EVIDENCE_ENTITY_DAO_ERROR_EXECUTE,
"Impossible de promouvoir lobservation");
database_statement_finalize(statement);
return success;
}
char *evidence_entity_dao_format_observations(EvidenceEntityDao *dao,
const char *evidence_identifier, GError **error)
{
static const char *sql =
"SELECT COALESCE(o.value_normalized,o.value_raw),o.entity_type,o.role,"
"o.source_header,o.occurrence,o.provenance_kind,o.verification_status,"
"o.integrated_at,o.entity_id,o.promotion_kind "
"FROM evidence_entity_observations o WHERE o.evidence_id=? "
"ORDER BY o.source_header,o.occurrence,1,o.role;";
DatabaseStatement *statement = NULL;
GString *text = NULL;
g_return_val_if_fail(error == NULL || *error == NULL, NULL);
if (!evidence_entity_dao_validate_list_request(dao, evidence_identifier,
"L'identifiant de preuve est invalide.", error)) return NULL;
statement = database_statement_prepare(dao->database, sql);
if (statement == NULL ||
!database_statement_bind_text(statement, 1, evidence_identifier))
goto failure;
text = g_string_new(NULL);
for (;;)
{
DatabaseStatementStepResult step = database_statement_step(statement);
if (step == DATABASE_STATEMENT_STEP_DONE) break;
if (step != DATABASE_STATEMENT_STEP_ROW) goto failure;
char *value = NULL, *type = NULL, *role = NULL, *header = NULL;
char *provenance = NULL, *status = NULL, *date = NULL;
char *entity = NULL, *promotion = NULL; int64_t occurrence = 0;
if (!database_statement_column_text(statement, 0, &value) ||
!database_statement_column_text(statement, 1, &type) ||
!database_statement_column_text(statement, 2, &role) ||
!database_statement_column_text(statement, 3, &header) ||
!database_statement_column_int64(statement, 4, &occurrence) ||
!database_statement_column_text(statement, 5, &provenance) ||
!database_statement_column_text(statement, 6, &status) ||
!database_statement_column_text(statement, 7, &date) ||
!database_statement_column_text(statement, 8, &entity) ||
!database_statement_column_text(statement, 9, &promotion))
{ g_free(value); g_free(type); g_free(role); g_free(header);
g_free(provenance); g_free(status); g_free(date); g_free(entity);
g_free(promotion); goto failure; }
g_string_append_printf(text, "%s%s — %s — rôle : %s — origine : %s"
" #%lld — provenance : %s — validation : %s — date : %s — "
"graphe : %s%s%s", text->len > 0 ? "\n" : "",
value, type, role, header, (long long) occurrence, provenance,
status, date, entity != NULL ? "promue" : "non ajoutée",
promotion != NULL ? " (" : "", promotion != NULL ? promotion : "");
if (promotion != NULL) g_string_append_c(text, ')');
g_free(value); g_free(type); g_free(role); g_free(header);
g_free(provenance); g_free(status); g_free(date); g_free(entity);
g_free(promotion);
}
database_statement_finalize(statement);
return g_string_free(text, FALSE);
failure:
evidence_entity_dao_set_database_error(dao, error,
EVIDENCE_ENTITY_DAO_ERROR_EXECUTE,
"Impossible de charger les observations preuve-entité");
database_statement_finalize(statement);
if (text != NULL) g_string_free(text, TRUE);
return NULL;
}
GPtrArray *evidence_entity_dao_list_observations(EvidenceEntityDao *dao,
const char *evidence_identifier, GError **error)
{
static const char *sql =
"SELECT id,COALESCE(value_normalized,value_raw),entity_type,role,"
"source_header,occurrence,provenance_kind,verification_status,"
"integrated_at,entity_id,promotion_kind "
"FROM evidence_entity_observations WHERE evidence_id=? "
"ORDER BY source_header,occurrence,2,role;";
DatabaseStatement *statement = NULL;
GPtrArray *items = NULL;
g_return_val_if_fail(error == NULL || *error == NULL, NULL);
if (!evidence_entity_dao_validate_list_request(dao, evidence_identifier,
"L'identifiant de preuve est invalide.", error)) return NULL;
statement = database_statement_prepare(dao->database, sql);
if (statement == NULL ||
!database_statement_bind_text(statement, 1, evidence_identifier))
goto failure;
items = g_ptr_array_new_with_free_func(
(GDestroyNotify) evidence_observation_free);
for (;;)
{
DatabaseStatementStepResult step = database_statement_step(statement);
if (step == DATABASE_STATEMENT_STEP_DONE) break;
if (step != DATABASE_STATEMENT_STEP_ROW) goto failure;
EvidenceObservation *item = g_new0(EvidenceObservation, 1);
int64_t occurrence = 0;
if (!database_statement_column_text(statement, 0, &item->identifier) ||
!database_statement_column_text(statement, 1, &item->value) ||
!database_statement_column_text(statement, 2, &item->type_identifier) ||
!database_statement_column_text(statement, 3, &item->role) ||
!database_statement_column_text(statement, 4, &item->source_header) ||
!database_statement_column_int64(statement, 5, &occurrence) ||
!database_statement_column_text(statement, 6, &item->provenance_kind) ||
!database_statement_column_text(statement, 7, &item->verification_status) ||
!database_statement_column_text(statement, 8, &item->integrated_at) ||
!database_statement_column_text(statement, 9, &item->entity_identifier) ||
!database_statement_column_text(statement, 10, &item->promotion_kind))
{ evidence_observation_free(item); goto failure; }
item->occurrence = (guint) occurrence;
g_ptr_array_add(items, item);
}
database_statement_finalize(statement);
return items;
failure:
evidence_entity_dao_set_database_error(dao, error,
EVIDENCE_ENTITY_DAO_ERROR_EXECUTE,
"Impossible de charger toutes les observations de la preuve");
database_statement_finalize(statement);
g_clear_pointer(&items, g_ptr_array_unref);
return NULL;
}

View file

@ -16,12 +16,12 @@
/**
* @brief Version actuelle du schéma SQLite.
*/
#define DATABASE_SCHEMA_VERSION_CURRENT 13
#define DATABASE_SCHEMA_VERSION_CURRENT 10
/**
* @brief Version actuelle sous forme textuelle pour metadata.
*/
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "13"
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "10"
/**
* @brief Nom de l'application enregistré dans les métadonnées.
@ -812,51 +812,6 @@ rollback:
return false;
}
static bool database_migrate_v10_to_v11(Database *database)
{
bool transaction_started = false;
if (database == NULL || !database_transaction_begin(database)) return false;
transaction_started = true;
if (!schema_install_v11(database) ||
!database_update_schema_version(database, "11") ||
!database_transaction_commit(database)) goto rollback;
return true;
rollback:
if (transaction_started && !database_transaction_rollback(database))
g_warning("Impossible dannuler la migration SQLite V10 vers V11.");
return false;
}
static bool database_migrate_v11_to_v12(Database *database)
{
bool transaction_started = false;
if (database == NULL || !database_transaction_begin(database)) return false;
transaction_started = true;
if (!schema_install_v12(database) ||
!database_update_schema_version(database, "12") ||
!database_transaction_commit(database)) goto rollback;
return true;
rollback:
if (transaction_started && !database_transaction_rollback(database))
g_warning("Impossible dannuler la migration SQLite V11 vers V12.");
return false;
}
static bool database_migrate_v12_to_v13(Database *database)
{
bool transaction_started = false;
if (database == NULL || !database_transaction_begin(database)) return false;
transaction_started = true;
if (!schema_install_v13(database) ||
!database_update_schema_version(database, "13") ||
!database_transaction_commit(database)) goto rollback;
return true;
rollback:
if (transaction_started && !database_transaction_rollback(database))
g_warning("Impossible dannuler la migration SQLite V12 vers V13.");
return false;
}
/**
* @brief Garantit atomiquement la présence des extensions du schéma courant.
*/
@ -1086,18 +1041,6 @@ bool database_migrate_to_latest(
if (!database_migrate_v9_to_v10(database)) return false;
schema_version = 10;
break;
case 10:
if (!database_migrate_v10_to_v11(database)) return false;
schema_version = 11;
break;
case 11:
if (!database_migrate_v11_to_v12(database)) return false;
schema_version = 12;
break;
case 12:
if (!database_migrate_v12_to_v13(database)) return false;
schema_version = 13;
break;
default:
database_set_error(

View file

@ -353,24 +353,6 @@ bool schema_install_v10(Database *database)
"la migration SQLite V10");
}
bool schema_install_v11(Database *database)
{
return schema_execute_file(database, "database/schema_v11.sql",
"la migration SQLite V11");
}
bool schema_install_v12(Database *database)
{
return schema_execute_file(database, "database/schema_v12.sql",
"la migration SQLite V12");
}
bool schema_install_v13(Database *database)
{
return schema_execute_file(database, "database/schema_v13.sql",
"la migration SQLite V13");
}
bool schema_ensure_current(
Database *database
)

View file

@ -1,11 +0,0 @@
#include "models/evidence_observation.h"
void evidence_observation_free(EvidenceObservation *observation)
{
if (observation == NULL) return;
g_free(observation->identifier); g_free(observation->value);
g_free(observation->type_identifier); g_free(observation->role);
g_free(observation->source_header); g_free(observation->provenance_kind);
g_free(observation->verification_status); g_free(observation->integrated_at);
g_free(observation->entity_identifier); g_free(observation->promotion_kind);
g_free(observation);
}

View file

@ -5,54 +5,8 @@
#include "views/eml_analysis_dialog.h"
#include "widgets/controlled_vocab_dropdown.h"
typedef struct { GtkWindow *window; GtkWidget *proposals_box;
GtkWidget *summary_label;
EmlAnalysisDialogCallback callback; gpointer user_data; gboolean completed;
} EmlAnalysisDialogState;
static void eml_analysis_dialog_update_summary(GtkCheckButton *button,
gpointer data)
{
EmlAnalysisDialogState *state = data; guint kept = 0, promoted = 0;
(void) button;
for (GtkWidget *row = gtk_widget_get_first_child(state->proposals_box);
row != NULL; row = gtk_widget_get_next_sibling(row))
{
GtkWidget *keep = g_object_get_data(G_OBJECT(row), "eml-keep");
GtkWidget *promote = g_object_get_data(G_OBJECT(row), "eml-promote");
gboolean active = keep != NULL &&
gtk_check_button_get_active(GTK_CHECK_BUTTON(keep));
if (promote != NULL)
{
gtk_widget_set_sensitive(promote, active);
if (!active)
gtk_check_button_set_active(GTK_CHECK_BUTTON(promote), FALSE);
}
if (active) kept++;
if (promote != NULL &&
gtk_check_button_get_active(GTK_CHECK_BUTTON(promote))) promoted++;
}
char *summary = g_strdup_printf(
"%u observation(s) seront conservée(s) dans la fiche ; "
"%u seront promue(s) en entité(s).", kept, promoted);
gtk_label_set_text(GTK_LABEL(state->summary_label), summary);
g_free(summary);
}
static void eml_analysis_dialog_bind_summary(EmlAnalysisDialogState *state,
GtkWidget *content)
{
state->summary_label = gtk_label_new(
"0 observation sera conservée dans la fiche ; "
"0 sera promue en entité.");
gtk_label_set_xalign(GTK_LABEL(state->summary_label), 0.0f);
gtk_box_append(GTK_BOX(content), state->summary_label);
for (GtkWidget *row = gtk_widget_get_first_child(state->proposals_box);
row != NULL; row = gtk_widget_get_next_sibling(row))
{
g_signal_connect(g_object_get_data(G_OBJECT(row), "eml-keep"),
"toggled", G_CALLBACK(eml_analysis_dialog_update_summary), state);
g_signal_connect(g_object_get_data(G_OBJECT(row), "eml-promote"),
"toggled", G_CALLBACK(eml_analysis_dialog_update_summary), state);
}
}
/** @brief Libère l'état de révision. */
static void eml_analysis_dialog_state_free(gpointer data) { g_free(data); }
/** @brief Signale une annulation une seule fois. */
@ -76,7 +30,7 @@ static void eml_analysis_dialog_on_integrate(GtkButton *button, gpointer data)
for (GtkWidget *child = gtk_widget_get_first_child(state->proposals_box);
child != NULL; child = gtk_widget_get_next_sibling(child))
{
GtkWidget *check = g_object_get_data(G_OBJECT(child), "eml-keep");
GtkWidget *check = GTK_IS_BOX(child) ? gtk_widget_get_first_child(child) : NULL;
if (check != NULL && GTK_IS_CHECK_BUTTON(check) &&
gtk_check_button_get_active(GTK_CHECK_BUTTON(check)))
{
@ -85,23 +39,10 @@ static void eml_analysis_dialog_on_integrate(GtkButton *button, gpointer data)
GtkWidget *status = g_object_get_data(G_OBJECT(child), "eml-status");
GtkWidget *provenance = g_object_get_data(G_OBJECT(child),
"eml-provenance");
const char *raw = g_object_get_data(G_OBJECT(child), "eml-raw");
const char *role = g_object_get_data(G_OBJECT(child), "eml-role");
const char *header = g_object_get_data(G_OBJECT(child), "eml-header");
guint occurrence = GPOINTER_TO_UINT(g_object_get_data(
G_OBJECT(child), "eml-occurrence"));
EmlEntityProposal *proposal = eml_entity_proposal_new_observation(
type, raw != NULL ? raw : value, value,
role != NULL ? role : "other",
header != NULL ? header : "manual",
occurrence > 0 ? occurrence : 1,
g_ptr_array_add(selected, eml_entity_proposal_new_with_metadata(
type, value,
controlled_vocab_dropdown_get_selected_code(status),
controlled_vocab_dropdown_get_selected_code(provenance));
GtkWidget *promote = g_object_get_data(G_OBJECT(child),
"eml-promote");
proposal->promote_to_entity = promote != NULL &&
gtk_check_button_get_active(GTK_CHECK_BUTTON(promote));
g_ptr_array_add(selected, proposal);
controlled_vocab_dropdown_get_selected_code(provenance)));
}
}
if (selected->len == 0)
@ -111,54 +52,6 @@ static void eml_analysis_dialog_on_integrate(GtkButton *button, gpointer data)
else g_ptr_array_unref(selected);
gtk_window_close(state->window);
}
static void eml_analysis_dialog_add_observations(GtkWidget *box,
const GPtrArray *observations)
{
for (guint i = 0; observations != NULL && i < observations->len; i++)
{
const EmlObservation *observation = g_ptr_array_index(
(GPtrArray *) observations, i);
char *text = g_strdup_printf("%s — %s — rôle : %s — origine : %s #%u",
observation->type_identifier, observation->value_normalized,
observation->role, observation->source_header,
observation->occurrence);
GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
GtkWidget *check = gtk_check_button_new_with_label(
"Conserver dans la fiche");
GtkWidget *value_label = gtk_label_new(text);
GtkWidget *promote = gtk_check_button_new_with_label(
"Promouvoir en entité");
gtk_widget_set_sensitive(promote, FALSE);
GtkWidget *status = controlled_vocab_dropdown_new(
CONTROLLED_VOCAB_VERIFICATION_STATUS, "proposed");
GtkWidget *provenance = controlled_vocab_dropdown_new(
CONTROLLED_VOCAB_PROVENANCE_KIND, observation->provenance_kind);
g_object_set_data_full(G_OBJECT(row), "eml-type",
g_strdup(observation->type_identifier), g_free);
g_object_set_data_full(G_OBJECT(row), "eml-value",
g_strdup(observation->value_normalized), g_free);
g_object_set_data_full(G_OBJECT(row), "eml-raw",
g_strdup(observation->value_raw), g_free);
g_object_set_data_full(G_OBJECT(row), "eml-role",
g_strdup(observation->role), g_free);
g_object_set_data_full(G_OBJECT(row), "eml-header",
g_strdup(observation->source_header), g_free);
g_object_set_data(G_OBJECT(row), "eml-occurrence",
GUINT_TO_POINTER(observation->occurrence));
g_object_set_data(G_OBJECT(row), "eml-status", status);
g_object_set_data(G_OBJECT(row), "eml-provenance", provenance);
g_object_set_data(G_OBJECT(row), "eml-keep", check);
g_object_set_data(G_OBJECT(row), "eml-promote", promote);
gtk_widget_set_hexpand(check, TRUE);
gtk_box_append(GTK_BOX(row), check);
gtk_box_append(GTK_BOX(row), value_label);
gtk_box_append(GTK_BOX(row), promote);
gtk_box_append(GTK_BOX(row), status);
gtk_box_append(GTK_BOX(row), provenance);
gtk_box_append(GTK_BOX(box), row);
g_free(text);
}
}
/** @brief Ajoute les propositions d'un type sous forme de cases décochées. */
static void eml_analysis_dialog_add_proposals(GtkWidget *box,
const char *type, const char *label, const GPtrArray *values)
@ -168,12 +61,7 @@ static void eml_analysis_dialog_add_proposals(GtkWidget *box,
const char *value = g_ptr_array_index((GPtrArray *) values, i);
char *text = g_strdup_printf("%s : %s", label, value);
GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
GtkWidget *check = gtk_check_button_new_with_label(
"Conserver dans la fiche");
GtkWidget *value_label = gtk_label_new(text);
GtkWidget *promote = gtk_check_button_new_with_label(
"Promouvoir en entité");
gtk_widget_set_sensitive(promote, FALSE);
GtkWidget *check = gtk_check_button_new_with_label(text);
GtkWidget *status = controlled_vocab_dropdown_new(
CONTROLLED_VOCAB_VERIFICATION_STATUS, "proposed");
GtkWidget *provenance = controlled_vocab_dropdown_new(
@ -183,12 +71,8 @@ static void eml_analysis_dialog_add_proposals(GtkWidget *box,
g_object_set_data_full(G_OBJECT(row), "eml-value", g_strdup(value), g_free);
g_object_set_data(G_OBJECT(row), "eml-status", status);
g_object_set_data(G_OBJECT(row), "eml-provenance", provenance);
g_object_set_data(G_OBJECT(row), "eml-keep", check);
g_object_set_data(G_OBJECT(row), "eml-promote", promote);
gtk_widget_set_hexpand(check, TRUE);
gtk_box_append(GTK_BOX(row), check);
gtk_box_append(GTK_BOX(row), value_label);
gtk_box_append(GTK_BOX(row), promote);
gtk_box_append(GTK_BOX(row), status);
gtk_box_append(GTK_BOX(row), provenance);
gtk_box_append(GTK_BOX(box), row); g_free(text);
@ -278,13 +162,17 @@ void eml_analysis_dialog_present(GtkWindow *parent,
destination_ips);
gtk_box_append(GTK_BOX(content), grid);
gtk_box_append(GTK_BOX(content), gtk_label_new(
"Choisir les observations à conserver ; la promotion vers le graphe "
"est une action séparée."));
"Sélection explicite des entités à intégrer"));
state->proposals_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
eml_analysis_dialog_add_observations(state->proposals_box,
eml_analysis_get_observations(analysis));
eml_analysis_dialog_add_proposals(state->proposals_box, "email_address",
"Email", eml_analysis_get_email_addresses(analysis));
eml_analysis_dialog_add_proposals(state->proposals_box, "domain_name",
"Domaine", eml_analysis_get_domains(analysis));
eml_analysis_dialog_add_proposals(state->proposals_box, "ip_address",
"IP expéditeur", eml_analysis_get_sender_ip_addresses(analysis));
eml_analysis_dialog_add_proposals(state->proposals_box, "ip_address",
"IP destinataire", eml_analysis_get_destination_ip_addresses(analysis));
gtk_box_append(GTK_BOX(content), state->proposals_box);
eml_analysis_dialog_bind_summary(state, content);
gtk_box_append(GTK_BOX(content), gtk_label_new("En-têtes bruts (lecture seule)"));
raw = gtk_text_view_new(); gtk_text_view_set_editable(GTK_TEXT_VIEW(raw), FALSE);
gtk_text_view_set_monospace(GTK_TEXT_VIEW(raw), TRUE);
@ -318,230 +206,3 @@ void eml_analysis_dialog_present(GtkWindow *parent,
g_free(received); g_free(emails); g_free(domains);
g_free(sender_ips); g_free(destination_ips);
}
static void eml_analysis_dialog_append_attachment_summary(
GString *text,
const EmlPipelineResult *result)
{
for (guint index = 0; result->mime_result != NULL &&
result->mime_result->attachments != NULL &&
index < result->mime_result->attachments->len; index++)
{
EmlAttachment *attachment = g_ptr_array_index(
result->mime_result->attachments, index);
g_string_append_printf(text,
"%s → %s | MIME %s / %s | partie %s | %"
G_GSIZE_FORMAT " octets | SHA-256 %s%s\n",
attachment->declared_filename != NULL
? attachment->declared_filename : "(sans nom)",
attachment->sanitized_filename != NULL
? attachment->sanitized_filename : "(sans nom)",
attachment->content_type != NULL
? attachment->content_type : "inconnu",
attachment->detected_mime != NULL
? attachment->detected_mime : "inconnu",
attachment->part_index != NULL ? attachment->part_index : "?",
attachment->decoded_size,
attachment->sha256 != NULL ? attachment->sha256 : "indisponible",
attachment->is_truncated ? " | TRONQUÉ" : "");
}
}
static void eml_analysis_dialog_append_document_summary(
GString *text,
GString *metadata,
GString *warnings,
const EmlPipelineResult *result)
{
for (guint index = 0; result->document_analyses != NULL &&
index < result->document_analyses->len; index++)
{
DocumentFileAnalysis *document = g_ptr_array_index(
result->document_analyses, index);
if (document->ocr != NULL && document->ocr->text != NULL)
g_string_append_printf(text, "\nOCR (%s)%s\n%s\n",
document->ocr->requested_languages,
document->ocr->execution->stdout_truncated
? " — texte tronqué/partiel" : "",
document->ocr->text);
if (document->pdf != NULL)
for (guint page_index = 0;
page_index < document->pdf->pages->len; page_index++)
{
PdfPageAnalysis *page = g_ptr_array_index(
document->pdf->pages, page_index);
g_string_append_printf(text,
"\nPDF page %u — méthode %s — état %s\n%s\n",
page->page_number,
page->method == PDF_PAGE_METHOD_NATIVE
? "texte natif" : "OCR",
document_analysis_state_code(page->state),
page->text != NULL ? page->text : "(aucun texte)");
}
if (document->metadata != NULL)
{
DocumentToolExecution *execution =
document->metadata->execution;
if (execution != NULL &&
execution->state == DOCUMENT_ANALYSIS_STATE_UNAVAILABLE)
g_string_append(warnings, "ExifTool indisponible.\n");
for (guint metadata_index = 0;
metadata_index < document->metadata->metadata->len;
metadata_index++)
{
DocumentMetadataEntry *entry = g_ptr_array_index(
document->metadata->metadata, metadata_index);
g_string_append_printf(metadata,
"%s | %s:%s | %s | %s%s\n",
entry->code, entry->original_group,
entry->original_tag, entry->raw_value,
execution != NULL && execution->version != NULL
? execution->version : "version inconnue",
entry->sensitive
? " | SENSIBLE — confirmation explicite requise" : "");
}
}
}
}
void eml_analysis_dialog_present_pipeline(
GtkWindow *parent,
const EmlPipelineResult *result,
const char *evidence_name,
const char *relative_path,
const char *source_sha256,
EmlAnalysisDialogCallback callback,
gpointer user_data)
{
if (parent == NULL || result == NULL || result->analysis == NULL) return;
EmlAnalysisDialogState *state = g_new0(EmlAnalysisDialogState, 1);
state->callback = callback; state->user_data = user_data;
state->window = GTK_WINDOW(gtk_window_new());
gtk_window_set_title(state->window, "Révision de lanalyse EML");
gtk_window_set_transient_for(state->window, parent);
gtk_window_set_modal(state->window, TRUE);
gtk_window_set_default_size(state->window, 900, 700);
GtkWidget *outer = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
gtk_widget_set_margin_start(outer, 14);
gtk_widget_set_margin_end(outer, 14);
gtk_widget_set_margin_top(outer, 14);
gtk_widget_set_margin_bottom(outer, 14);
GtkWidget *content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
GtkWidget *grid = gtk_grid_new();
gtk_grid_set_row_spacing(GTK_GRID(grid), 6);
gtk_grid_set_column_spacing(GTK_GRID(grid), 12);
eml_analysis_dialog_add_field(GTK_GRID(grid), 0, "Preuve", evidence_name);
eml_analysis_dialog_add_field(GTK_GRID(grid), 1, "Chemin relatif", relative_path);
eml_analysis_dialog_add_field(GTK_GRID(grid), 2, "SHA-256", source_sha256);
eml_analysis_dialog_add_field(GTK_GRID(grid), 3, "État global",
document_analysis_state_code(result->state));
eml_analysis_dialog_add_field(GTK_GRID(grid), 4, "From",
eml_analysis_get_first_header(result->analysis, "from"));
eml_analysis_dialog_add_field(GTK_GRID(grid), 5, "Sender",
eml_analysis_get_first_header(result->analysis, "sender"));
eml_analysis_dialog_add_field(GTK_GRID(grid), 6, "Reply-To",
eml_analysis_get_first_header(result->analysis, "reply-to"));
eml_analysis_dialog_add_field(GTK_GRID(grid), 7, "Return-Path",
eml_analysis_get_first_header(result->analysis, "return-path"));
eml_analysis_dialog_add_field(GTK_GRID(grid), 8, "To",
eml_analysis_get_first_header(result->analysis, "to"));
eml_analysis_dialog_add_field(GTK_GRID(grid), 9, "Cc",
eml_analysis_get_first_header(result->analysis, "cc"));
eml_analysis_dialog_add_field(GTK_GRID(grid), 10, "Subject",
eml_analysis_get_first_header(result->analysis, "subject"));
eml_analysis_dialog_add_field(GTK_GRID(grid), 11, "Date brute",
eml_analysis_get_first_header(result->analysis, "date"));
eml_analysis_dialog_add_field(GTK_GRID(grid), 12, "Message-ID",
eml_analysis_get_first_header(result->analysis, "message-id"));
char *received = eml_analysis_dialog_join(
eml_analysis_get_header_values(result->analysis, "received"));
eml_analysis_dialog_add_field(GTK_GRID(grid), 13,
"Received (ordre original)", received);
gtk_box_append(GTK_BOX(content), grid);
GString *attachments = g_string_new(NULL);
GString *texts = g_string_new(NULL);
GString *metadata = g_string_new(NULL);
GString *warnings = g_string_new(NULL);
eml_analysis_dialog_append_attachment_summary(attachments, result);
eml_analysis_dialog_append_document_summary(
texts, metadata, warnings, result);
for (guint index = 0; result->warnings != NULL &&
index < result->warnings->len; index++)
g_string_append_printf(warnings, "%s\n",
(char *) g_ptr_array_index(result->warnings, index));
for (guint index = 0; result->bank_proposals != NULL &&
index < result->bank_proposals->len; index++)
{
BankProposal *proposal = g_ptr_array_index(
result->bank_proposals, index);
if (!proposal->is_iban_valid ||
proposal->suggested_ocr_fix != NULL)
g_string_append_printf(warnings,
"Proposition bancaire invalide ou corrigée, "
"désélectionnée : %s\n",
proposal->raw_iban != NULL ? proposal->raw_iban : "(vide)");
}
GtkWidget *details = gtk_grid_new();
gtk_grid_set_row_spacing(GTK_GRID(details), 6);
gtk_grid_set_column_spacing(GTK_GRID(details), 12);
eml_analysis_dialog_add_field(GTK_GRID(details), 0,
"Pièces jointes", attachments->str);
eml_analysis_dialog_add_field(GTK_GRID(details), 1,
"Texte PDF et OCR", texts->str);
eml_analysis_dialog_add_field(GTK_GRID(details), 2,
"Métadonnées", metadata->str);
eml_analysis_dialog_add_field(GTK_GRID(details), 3,
"Avertissements", warnings->str);
gtk_box_append(GTK_BOX(content), details);
state->proposals_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
eml_analysis_dialog_add_observations(state->proposals_box,
eml_analysis_get_observations(result->analysis));
GPtrArray *valid_ibans = g_ptr_array_new();
for (guint index = 0; result->bank_proposals != NULL &&
index < result->bank_proposals->len; index++)
{
BankProposal *proposal = g_ptr_array_index(
result->bank_proposals, index);
if (proposal->is_iban_valid && proposal->normalized_iban != NULL &&
proposal->suggested_ocr_fix == NULL)
g_ptr_array_add(valid_ibans, proposal->normalized_iban);
}
eml_analysis_dialog_add_proposals(state->proposals_box, "iban",
"IBAN valide", valid_ibans);
g_ptr_array_unref(valid_ibans);
gtk_box_append(GTK_BOX(content), gtk_label_new(
"Propositions à intégrer explicitement"));
gtk_box_append(GTK_BOX(content), state->proposals_box);
eml_analysis_dialog_bind_summary(state, content);
GtkWidget *scroll = gtk_scrolled_window_new();
gtk_widget_set_vexpand(scroll, TRUE);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), content);
gtk_box_append(GTK_BOX(outer), scroll);
GtkWidget *actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
gtk_widget_set_halign(actions, GTK_ALIGN_END);
GtkWidget *close = gtk_button_new_with_label("Rejeter et fermer");
GtkWidget *integrate = gtk_button_new_with_label(
"Intégrer les éléments sélectionnés");
gtk_widget_add_css_class(integrate, "suggested-action");
g_signal_connect(close, "clicked",
G_CALLBACK(eml_analysis_dialog_on_close), state);
g_signal_connect(integrate, "clicked",
G_CALLBACK(eml_analysis_dialog_on_integrate), state);
gtk_box_append(GTK_BOX(actions), close);
gtk_box_append(GTK_BOX(actions), integrate);
gtk_box_append(GTK_BOX(outer), actions);
gtk_window_set_child(state->window, outer);
g_signal_connect(state->window, "close-request",
G_CALLBACK(eml_analysis_dialog_on_window_close), state);
g_object_set_data_full(G_OBJECT(state->window), "eml-dialog-state",
state, eml_analysis_dialog_state_free);
gtk_window_present(state->window);
g_free(received);
g_string_free(attachments, TRUE);
g_string_free(texts, TRUE);
g_string_free(metadata, TRUE);
g_string_free(warnings, TRUE);
}

View file

@ -1995,31 +1995,6 @@ void main_window_set_selected_evidence(
);
}
void main_window_set_evidence_observations(MainWindow *main_window,
const GPtrArray *observations)
{
if (main_window != NULL)
workspace_set_evidence_observations(main_window->workspace,
observations);
}
void main_window_set_observation_remove_callback(MainWindow *main_window,
void (*callback)(const char *, gpointer), gpointer user_data)
{
if (main_window != NULL)
workspace_set_observation_remove_callback(main_window->workspace,
callback, user_data);
}
void main_window_set_eml_analysis_available(
MainWindow *main_window,
gboolean available)
{
if (main_window != NULL)
workspace_set_eml_analysis_available(
main_window->workspace, available);
}
void main_window_set_evidence_preview(MainWindow *main_window,
const char *file_path, const char *display_name)
{

View file

@ -90,7 +90,6 @@ struct Workspace
GtkWidget *evidence_internal_name_label;
GtkWidget *evidence_identifier_label;
GtkWidget *evidence_sha256_label;
GtkWidget *evidence_observations_box;
GtkWidget *verify_evidence_button;
GtkWidget *edit_evidence_button;
GtkWidget *analyze_eml_button;
@ -124,8 +123,6 @@ struct Workspace
gpointer extract_metadata_user_data;
WorkspaceRecoverPdfPasswordCallback recover_pdf_password_callback;
gpointer recover_pdf_password_user_data;
WorkspaceObservationRemoveCallback observation_remove_callback;
gpointer observation_remove_user_data;
WorkspaceGraphNodeMovedCallback
graph_node_moved_callback;
@ -1601,11 +1598,11 @@ Workspace *workspace_new(void)
G_CALLBACK(workspace_on_edit_evidence_clicked), workspace);
gtk_box_append(GTK_BOX(evidence_content), workspace->edit_evidence_button);
workspace->analyze_eml_button = gtk_button_new_with_label(
"Analyser le-mail");
"Analyser les en-têtes EML");
gtk_widget_set_halign(workspace->analyze_eml_button, GTK_ALIGN_START);
gtk_widget_set_sensitive(workspace->analyze_eml_button, FALSE);
gtk_widget_set_tooltip_text(workspace->analyze_eml_button,
"Vérifier la preuve puis lancer lanalyse complète en arrière-plan");
"Créer une copie vérifiée puis analyser localement ses en-têtes");
g_signal_connect(workspace->analyze_eml_button, "clicked",
G_CALLBACK(workspace_on_analyze_eml_clicked), workspace);
gtk_box_append(GTK_BOX(evidence_content), workspace->analyze_eml_button);
@ -1749,15 +1746,6 @@ Workspace *workspace_new(void)
10,
"SHA-256"
);
GtkWidget *observations_title = gtk_label_new(
"Observations extraites");
gtk_label_set_xalign(GTK_LABEL(observations_title), 0.0f);
gtk_widget_add_css_class(observations_title, "heading");
workspace->evidence_observations_box =
gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
gtk_grid_attach(GTK_GRID(evidence_grid), observations_title, 0, 11, 1, 1);
gtk_grid_attach(GTK_GRID(evidence_grid),
workspace->evidence_observations_box, 1, 11, 1, 1);
if (workspace->evidence_type_label == NULL ||
workspace->evidence_integrity_label == NULL ||
@ -1769,8 +1757,7 @@ Workspace *workspace_new(void)
workspace->evidence_relative_path_label == NULL ||
workspace->evidence_internal_name_label == NULL ||
workspace->evidence_identifier_label == NULL ||
workspace->evidence_sha256_label == NULL ||
workspace->evidence_observations_box == NULL)
workspace->evidence_sha256_label == NULL)
{
workspace_free(
workspace
@ -2589,82 +2576,6 @@ Workspace *workspace_new(void)
return workspace;
}
static void workspace_on_remove_observation_clicked(GtkButton *button,
gpointer user_data)
{
Workspace *workspace = user_data;
const char *identifier = g_object_get_data(G_OBJECT(button),
"observation-identifier");
if (workspace != NULL && workspace->observation_remove_callback != NULL)
workspace->observation_remove_callback(identifier,
workspace->observation_remove_user_data);
}
void workspace_set_evidence_observations(Workspace *workspace,
const GPtrArray *observations)
{
if (workspace == NULL || workspace->evidence_observations_box == NULL)
return;
GtkWidget *child = gtk_widget_get_first_child(
workspace->evidence_observations_box);
while (child != NULL)
{
GtkWidget *next = gtk_widget_get_next_sibling(child);
gtk_box_remove(GTK_BOX(workspace->evidence_observations_box), child);
child = next;
}
if (observations == NULL || observations->len == 0)
{
gtk_box_append(GTK_BOX(workspace->evidence_observations_box),
gtk_label_new("Aucune observation confirmée pour cette preuve."));
return;
}
for (guint index = 0; index < observations->len; index++)
{
const EvidenceObservation *item = g_ptr_array_index(
(GPtrArray *) observations, index);
GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
char *text = g_strdup_printf(
"%s\nType : %s — rôle : %s\nSource : %s #%u — provenance : %s\n"
"Validation : %s — intégrée : %s\nGraphe : %s",
item->value, item->type_identifier, item->role,
item->source_header, item->occurrence, item->provenance_kind,
item->verification_status, item->integrated_at,
item->entity_identifier == NULL ? "Non ajoutée" :
(g_strcmp0(item->promotion_kind, "created") == 0
? "Entité créée" :
g_strcmp0(item->promotion_kind, "reused") == 0
? "Entité existante réutilisée" : "Promotion historique"));
GtkWidget *label = gtk_label_new(text);
gtk_label_set_xalign(GTK_LABEL(label), 0.0f);
gtk_label_set_selectable(GTK_LABEL(label), TRUE);
gtk_widget_set_hexpand(label, TRUE);
gtk_box_append(GTK_BOX(row), label);
if (item->entity_identifier != NULL)
{
GtkWidget *remove = gtk_button_new_with_label("Retirer du graphe");
gtk_widget_add_css_class(remove, "flat");
gtk_widget_set_tooltip_text(remove,
"Détache cette observation sans supprimer son contenu.");
g_object_set_data_full(G_OBJECT(remove), "observation-identifier",
g_strdup(item->identifier), g_free);
g_signal_connect(remove, "clicked",
G_CALLBACK(workspace_on_remove_observation_clicked), workspace);
gtk_box_append(GTK_BOX(row), remove);
}
gtk_box_append(GTK_BOX(workspace->evidence_observations_box), row);
g_free(text);
}
}
void workspace_set_observation_remove_callback(Workspace *workspace,
WorkspaceObservationRemoveCallback callback, gpointer user_data)
{
if (workspace == NULL) return;
workspace->observation_remove_callback = callback;
workspace->observation_remove_user_data = user_data;
}
GtkWidget *workspace_get_widget(
const Workspace *workspace
)
@ -3039,15 +2950,6 @@ void workspace_set_selected_evidence(
);
}
void workspace_set_eml_analysis_available(
Workspace *workspace,
gboolean available)
{
if (workspace != NULL && workspace->analyze_eml_button != NULL)
gtk_widget_set_sensitive(
workspace->analyze_eml_button, available);
}
void workspace_set_evidence_preview(Workspace *workspace,
const char *file_path, const char *display_name)
{

View file

@ -1,158 +0,0 @@
/******************************************************************************
* @file fake_document_tool.c
* @brief Faux outil documentaire synthétique, sans shell.
******************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
#include <unistd.h>
static int has_argument(int argc, char **argv, const char *value)
{
for (int index = 1; index < argc; index++)
if (strcmp(argv[index], value) == 0)
return 1;
return 0;
}
static const char *find_pdf_path(int argc, char **argv)
{
for (int index = 1; index < argc; index++)
if (strstr(argv[index], ".pdf") != NULL)
return argv[index];
return "";
}
static long argument_long(
int argc,
char **argv,
const char *name,
long fallback
)
{
for (int index = 1; index + 1 < argc; index++)
if (strcmp(argv[index], name) == 0)
return strtol(argv[index + 1], NULL, 10);
return fallback;
}
static void write_repeated(FILE *stream, char value, long count)
{
char block[1024];
memset(block, value, sizeof(block));
while (count > 0)
{
size_t amount = (size_t) (count > (long) sizeof(block)
? (long) sizeof(block) : count);
if (fwrite(block, 1, amount, stream) != amount)
return;
fflush(stream);
count -= (long) amount;
}
}
int main(int argc, char **argv)
{
if (has_argument(argc, argv, "--emit"))
{
long stdout_size = argument_long(
argc, argv, "--stdout-size", 0);
long stderr_size = argument_long(
argc, argv, "--stderr-size", 0);
long chunks = argument_long(argc, argv, "--chunks", 1);
int exit_status = (int) argument_long(
argc, argv, "--exit-status", 0);
if (chunks < 1)
chunks = 1;
for (long index = 0; index < chunks; index++)
{
write_repeated(stdout, 'O', stdout_size / chunks);
write_repeated(stderr, 'E', stderr_size / chunks);
if (has_argument(argc, argv, "--slow"))
(void) poll(NULL, 0, 20);
}
return exit_status;
}
if (has_argument(argc, argv, "-ver"))
{
puts("13.00");
return 0;
}
if (has_argument(argc, argv, "--version"))
{
puts("tesseract 5.0.0-synthetic");
return 0;
}
if (has_argument(argc, argv, "-j"))
{
if (strstr(argv[argc - 1], "slow") != NULL)
sleep(2);
if (strstr(argv[argc - 1], "large") != NULL)
{
fputs("[{\"File:MIMEType\":\"image/png\",\"Padding\":\"", stdout);
write_repeated(stdout, 'X', 8192);
return 0;
}
puts("[{\"File:MIMEType\":\"image/png\","
"\"File:FileSize\":42,\"EXIF:ImageWidth\":10,"
"\"EXIF:GPSLatitude\":48.5,\"EXIF:GPSLongitude\":2.2,"
"\"EXIF:SyntheticBoolean\":true}]");
return 0;
}
if (has_argument(argc, argv, "-enc"))
{
const char *path = find_pdf_path(argc, argv);
if (strstr(path, "slow-text") != NULL)
sleep(2);
if (strstr(path, "native") != NULL)
{
puts("Texte synthétique suffisamment long pour être considéré "
"comme exploitable dans cette fixture.\fDeuxième page.");
}
return 0;
}
if (has_argument(argc, argv, "-singlefile"))
{
const char *pdf_path = find_pdf_path(argc, argv);
if (strstr(pdf_path, "slow-render") != NULL)
sleep(2);
if (strstr(pdf_path, "slow-page-2") != NULL &&
strcmp(argv[2], "2") == 0)
sleep(2);
const char *prefix = argv[argc - 1];
char path[4096];
if (snprintf(path, sizeof(path), "%s.png", prefix) < 0)
return 2;
FILE *file = fopen(path, "wb");
if (file == NULL)
return 3;
fputs("synthetic-image", file);
fclose(file);
return 0;
}
if (has_argument(argc, argv, "stdout"))
{
if (strstr(argv[1], "sleep") != NULL ||
strstr(argv[1], "slow-ocr") != NULL)
sleep(2);
if (strstr(argv[1], "large") != NULL)
{
write_repeated(stdout, 'T', 8192);
return 0;
}
if (strstr(argv[1], "page-2") != NULL)
puts("Texte OCR synthétique page deux.");
else
puts("Texte OCR synthétique page une.");
return 0;
}
const char *path = find_pdf_path(argc, argv);
if (strstr(path, "slow-info") != NULL)
sleep(2);
if (strstr(path, "encrypted") != NULL)
puts("Pages: 2\nEncrypted: yes");
else
puts("Pages: 2\nEncrypted: no");
return 0;
}

View file

@ -1,38 +0,0 @@
From: Expediteur Demo <sender@example.com>
Sender: Service Demo <service@example.net>
Reply-To: Reponse Differente <reply@example.org>
Return-Path: <bounce@example.com>
To: Destinataire Demo <recipient@example.net>
Cc: Controle Demo <audit@example.org>
Subject: Démonstration synthétique du parcours EML
Date: Tue, 28 Jul 2026 10:30:00 +0200
Message-ID: <manual-smoke-test-107@example.com>
Received: from relay2.example.net (relay2.example.net [198.51.100.20]) by mx.example.org with ESMTP id DEMO2
Received: from relay1.example.com (relay1.example.com [192.0.2.10]) by relay2.example.net with ESMTP id DEMO1
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="LABFY-DEMO-107"
--LABFY-DEMO-107
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Ceci est un message entièrement synthétique destiné au test manuel.
IBAN DE DÉMONSTRATION VOLONTAIREMENT INVALIDE : FR00 0000 0000 0000 0000 0000 000
BIC SYNTHÉTIQUE DE FORMAT CONTRÔLABLE : DEMOFRPP
--LABFY-DEMO-107
Content-Type: text/plain; name="demonstration.txt"
Content-Disposition: attachment; filename="demonstration.txt"
Content-Transfer-Encoding: base64
RmljaGllciBzeW50aMOpdGlxdWUgZGUgZMOpbW9uc3RyYXRpb24uIElCQU4gREVNTyBJTlZBTElERTogRlIwMCAwMDAwIDAwMDAgMDAwMCAwMDAwIDAwMDAgMDAwLiBCSUMgREVNT0ZSUFAu
--LABFY-DEMO-107
Content-Type: image/png; name="pixel_synthetique.png"
Content-Disposition: inline; filename="pixel_synthetique.png"
Content-ID: <pixel-demo@example.com>
Content-Transfer-Encoding: base64
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=
--LABFY-DEMO-107--

View file

@ -47,33 +47,6 @@ static void test_french_rib_derivation(void)
bank_proposal_free(proposal);
}
static void test_structured_values(void)
{
static const char text[] =
"IBAN : FR48 3000 2005 5000 0000 0000 052\n"
"BIC : bnpafrppxxx\n"
"Titulaire : Élodie Exemple\n"
"Banque : Banque Synthétique\n"
"Adresse de la banque : 1 rue des Tests\n";
BankProposal *proposal = bank_proposal_analyze_text(
text,
"synthetic-evidence"
);
g_assert_nonnull(proposal);
g_assert_cmpstr(proposal->raw_iban, ==,
"FR48 3000 2005 5000 0000 0000 052");
g_assert_cmpstr(proposal->normalized_iban, ==,
"FR4830002005500000000000052");
g_assert_cmpstr(proposal->raw_bic, ==, "bnpafrppxxx");
g_assert_cmpstr(proposal->bic, ==, "BNPAFRPPXXX");
g_assert_cmpstr(proposal->holder_name, ==, "Élodie Exemple");
g_assert_cmpstr(proposal->bank_name, ==, "Banque Synthétique");
g_assert_cmpstr(proposal->bank_address, ==, "1 rue des Tests");
g_assert_cmpstr(proposal->iban_validation, ==, "valid");
bank_proposal_free(proposal);
}
int main(int argc, char **argv)
{
@ -81,6 +54,5 @@ int main(int argc, char **argv)
g_test_add_func("/bank-proposal/iban-validation", test_iban_validation);
g_test_add_func("/bank-proposal/bic-validation", test_bic_validation);
g_test_add_func("/bank-proposal/french-rib-derivation", test_french_rib_derivation);
g_test_add_func("/bank-proposal/structured-values", test_structured_values);
return g_test_run();
}

View file

@ -525,7 +525,7 @@ static void test_database_initialize_valid_database(void)
"FROM investigation;"
);
assert(strcmp(schema_version, "13") == 0);
assert(strcmp(schema_version, "10") == 0);
test_database_assert_table_exists(database, "bank_account_entities");
test_database_assert_table_exists(database, "relation_types");
test_database_assert_table_exists(database, "graph_viewport");
@ -992,7 +992,7 @@ static void test_database_migrate_v1_to_v2(void)
assert(
strcmp(
schema_version,
"13"
"10"
) == 0
);
@ -1399,54 +1399,6 @@ static void test_database_migration_rollback(void)
);
}
static void test_database_migrate_v12_to_v13_preserves_legacy_link(void)
{
GError *error = NULL;
char *directory = g_dir_make_tmp("labfy-v13-test-XXXXXX", &error);
char *path = g_build_filename(directory, "Enquete.sqlite", NULL);
sqlite3 *sqlite_database = NULL;
Database *database = NULL;
char *version = NULL;
char *legacy_sources = NULL;
assert(directory != NULL && error == NULL);
assert(database_initialize(path, "Migration V13 synthétique", directory));
assert(sqlite3_open(path, &sqlite_database) == SQLITE_OK);
test_database_execute_sql(sqlite_database,
"DROP TABLE preuve_entite_sources;"
"UPDATE metadata SET value='12' WHERE key='schema_version';"
"INSERT INTO preuves(id,name,relative_path,type_id,size_bytes,sha256,"
"imported_at,updated_at,status,locked,original_name) VALUES("
"'10000000-0000-4000-8000-000000000013','legacy.eml','legacy.eml',"
"5,1,'0000000000000000000000000000000000000000000000000000000000000000',"
"'2026-07-28T08:00:00Z','2026-07-28T08:00:00Z','active',0,'legacy.eml');"
"INSERT INTO entites(id,type_id,valeur,label,confiance,created_at,"
"updated_at,status) VALUES("
"'20000000-0000-4000-8000-000000000013',1,'legacy@example.test',"
"'legacy@example.test',50,'2026-07-28T08:00:00Z',"
"'2026-07-28T08:00:00Z','active');"
"INSERT INTO preuve_entites(preuve_id,entite_id) VALUES("
"'10000000-0000-4000-8000-000000000013',"
"'20000000-0000-4000-8000-000000000013');");
assert(sqlite3_close(sqlite_database) == SQLITE_OK);
database = database_open(path);
assert(database != NULL && database_migrate_to_latest(database));
database_close(database);
assert(sqlite3_open(path, &sqlite_database) == SQLITE_OK);
version = test_database_read_single_text(sqlite_database,
"SELECT value FROM metadata WHERE key='schema_version';");
legacy_sources = test_database_read_single_text(sqlite_database,
"SELECT COUNT(*) FROM preuve_entite_sources "
"WHERE source_kind='legacy_manual';");
assert(strcmp(version, "13") == 0);
assert(strcmp(legacy_sources, "1") == 0);
assert(sqlite3_close(sqlite_database) == SQLITE_OK);
assert(g_remove(path) == 0 && g_rmdir(directory) == 0);
g_free(legacy_sources);
g_free(version);
g_free(path);
g_free(directory);
}
int main(void)
{
test_database_initialize_valid_database();
@ -1454,7 +1406,6 @@ int main(void)
test_database_migrate_v1_to_v2();
test_database_migration_rollback();
test_database_migrate_v12_to_v13_preserves_legacy_link();
test_database_initialize_invalid_parameters();
test_database_initialize_missing_parent();

View file

@ -1,160 +0,0 @@
/******************************************************************************
* @file test_document_tool_runner.c
* @brief Tests synthétiques de la capture documentaire bornée.
******************************************************************************/
#include "core/document_tool_runner.h"
#include <glib.h>
#include <glib/gstdio.h>
#include <sys/resource.h>
static char *create_source(char **out_directory)
{
GError *error = NULL;
*out_directory = g_dir_make_tmp("labfy-runner-XXXXXX", &error);
g_assert_no_error(error);
char *path = g_build_filename(*out_directory, "source.bin", NULL);
g_assert_true(g_file_set_contents(path, "synthetic", -1, &error));
g_assert_no_error(error);
return path;
}
static void remove_source(char *directory, char *path)
{
g_remove(path);
g_rmdir(directory);
g_free(path);
g_free(directory);
}
static void test_limits_and_concurrent_drain(void)
{
char *directory = NULL;
char *path = create_source(&directory);
const char *arguments[] = {
"--emit", "--stdout-size", "5000", "--stderr-size", "7000",
"--chunks", "20", "--exit-status", "7", NULL
};
DocumentToolRunnerLimits limits = { 1000, 1500 };
DocumentToolExecution *execution = NULL;
GError *error = NULL;
g_assert_true(document_tool_runner_run_with_limits(
"synthetic", "tests/fake_document_tool", arguments, path,
&limits, NULL, &execution, &error));
g_assert_no_error(error);
g_assert_cmpuint(strlen(execution->raw_stdout), ==, 1000);
g_assert_cmpuint(strlen(execution->raw_stderr), ==, 1500);
g_assert_cmpuint(execution->stdout_bytes_observed, ==, 5000);
g_assert_cmpuint(execution->stderr_bytes_observed, ==, 7000);
g_assert_true(execution->stdout_truncated);
g_assert_true(execution->stderr_truncated);
g_assert_cmpint(execution->exit_status, ==, 7);
g_assert_cmpint(execution->state, ==,
DOCUMENT_ANALYSIS_STATE_PARTIAL);
g_assert_cmpint(execution->raw_stdout[0], ==, 'O');
g_assert_cmpint(execution->raw_stderr[0], ==, 'E');
document_tool_execution_free(execution);
remove_source(directory, path);
}
static void test_exact_and_below_limits(void)
{
char *directory = NULL;
char *path = create_source(&directory);
const char *arguments[] = {
"--emit", "--stdout-size", "1000", "--stderr-size", "12", NULL
};
DocumentToolRunnerLimits limits = { 1000, 20 };
DocumentToolExecution *execution = NULL;
g_assert_true(document_tool_runner_run_with_limits(
"synthetic", "tests/fake_document_tool", arguments, path,
&limits, NULL, &execution, NULL));
g_assert_false(execution->stdout_truncated);
g_assert_false(execution->stderr_truncated);
g_assert_cmpint(execution->state, ==,
DOCUMENT_ANALYSIS_STATE_SUCCESS);
document_tool_execution_free(execution);
remove_source(directory, path);
}
static gpointer cancel_later(gpointer user_data)
{
g_usleep(60000);
g_cancellable_cancel(user_data);
return NULL;
}
static void test_cancellation_during_output(void)
{
char *directory = NULL;
char *path = create_source(&directory);
const char *arguments[] = {
"--emit", "--stdout-size", "100000", "--stderr-size", "100000",
"--chunks", "100", "--slow", NULL
};
DocumentToolRunnerLimits limits = { 128, 128 };
DocumentToolExecution *execution = NULL;
GCancellable *cancellable = g_cancellable_new();
GThread *thread = g_thread_new("cancel-runner", cancel_later, cancellable);
GError *error = NULL;
g_assert_false(document_tool_runner_run_with_limits(
"synthetic", "tests/fake_document_tool", arguments, path,
&limits, cancellable, &execution, &error));
g_thread_join(thread);
g_assert_error(error, G_IO_ERROR, G_IO_ERROR_CANCELLED);
g_assert_nonnull(execution);
g_assert_cmpint(execution->state, ==,
DOCUMENT_ANALYSIS_STATE_CANCELLED);
g_clear_error(&error);
document_tool_execution_free(execution);
g_object_unref(cancellable);
const char *success_arguments[] = {
"--emit", "--stdout-size", "1", NULL
};
execution = NULL;
g_assert_true(document_tool_runner_run_with_limits(
"synthetic", "tests/fake_document_tool", success_arguments, path,
&limits, NULL, &execution, NULL));
document_tool_execution_free(execution);
remove_source(directory, path);
}
static void test_repeated_runs_do_not_exhaust_descriptors(void)
{
char *directory = NULL;
char *path = create_source(&directory);
const char *arguments[] = {
"--emit", "--stdout-size", "8", "--stderr-size", "8", NULL
};
DocumentToolRunnerLimits limits = { 16, 16 };
struct rlimit original_limit;
g_assert_cmpint(getrlimit(RLIMIT_NOFILE, &original_limit), ==, 0);
struct rlimit test_limit = original_limit;
test_limit.rlim_cur = MIN(original_limit.rlim_cur, (rlim_t) 64);
g_assert_cmpint(setrlimit(RLIMIT_NOFILE, &test_limit), ==, 0);
for (guint index = 0; index < 96; index++)
{
DocumentToolExecution *execution = NULL;
g_assert_true(document_tool_runner_run_with_limits(
"synthetic", "tests/fake_document_tool", arguments, path,
&limits, NULL, &execution, NULL));
document_tool_execution_free(execution);
}
g_assert_cmpint(setrlimit(RLIMIT_NOFILE, &original_limit), ==, 0);
remove_source(directory, path);
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
g_test_add_func("/document-tool-runner/limits-concurrent",
test_limits_and_concurrent_drain);
g_test_add_func("/document-tool-runner/exact-below",
test_exact_and_below_limits);
g_test_add_func("/document-tool-runner/cancellation",
test_cancellation_during_output);
g_test_add_func("/document-tool-runner/no-descriptor-exhaustion",
test_repeated_runs_do_not_exhaust_descriptors);
return g_test_run();
}

View file

@ -16,9 +16,7 @@ static void test_eml_analyzer_headers(void)
"Reply-To: replies@reply.test\r\n"
"To: victim@example.net\r\n"
"Subject: Synthetic fixture\r\n"
"Date: Wed, 22 Jul 2026 12:00:00 +0200\r\n"
"Message-ID: <id-123@example.test>\r\n"
"MIME-Version: 1.0\r\n"
"Received: from mail.example.test (mail.example.test [192.0.2.10])\r\n"
" by mx.example.net ([198.51.100.20]) with ESMTP; Wed, 22 Jul 2026 10:00:00 +0000\r\n"
"Received: from localhost ([127.0.0.1]) by mail.example.test\r\n"
@ -28,7 +26,6 @@ static void test_eml_analyzer_headers(void)
EmlAnalysis *analysis = NULL;
const GPtrArray *received = NULL, *emails = NULL, *ips = NULL;
const GPtrArray *sender_ips = NULL, *destination_ips = NULL;
const GPtrArray *domains = NULL, *observations = NULL;
GError *error = NULL;
directory = g_dir_make_tmp("labfy-eml-test-XXXXXX", &error);
assert(directory != NULL && error == NULL);
@ -38,8 +35,6 @@ static void test_eml_analyzer_headers(void)
assert(analysis != NULL && error == NULL);
assert(strcmp(eml_analysis_get_first_header(analysis, "from"),
"Example Sender <sender@example.test>") == 0);
assert(strcmp(eml_analysis_get_date_utc(analysis),
"2026-07-22T10:00:00Z") == 0);
received = eml_analysis_get_header_values(analysis, "Received");
assert(received != NULL && received->len == 2);
assert(strstr(g_ptr_array_index((GPtrArray *) received, 0), " by mx.example.net") != NULL);
@ -58,56 +53,12 @@ static void test_eml_analyzer_headers(void)
"192.0.2.10") == 0);
assert(strcmp(g_ptr_array_index((GPtrArray *) destination_ips, 0),
"198.51.100.20") == 0);
domains = eml_analysis_get_domains(analysis);
for (guint i = 0; i < domains->len; i++)
{
const char *domain = g_ptr_array_index((GPtrArray *) domains, i);
assert(strcmp(domain, "192.0.2.10") != 0);
assert(strcmp(domain, "198.51.100.20") != 0);
assert(strcmp(domain, "1.0") != 0);
}
observations = eml_analysis_get_observations(analysis);
assert(observations != NULL && observations->len > 0);
gboolean found_from = FALSE, found_received_ip = FALSE;
for (guint i = 0; i < observations->len; i++)
{
const EmlObservation *observation = g_ptr_array_index(
(GPtrArray *) observations, i);
if (strcmp(observation->value_normalized, "sender@example.test") == 0 &&
strcmp(observation->role, "from") == 0 &&
strcmp(observation->source_header, "from") == 0)
found_from = TRUE;
if (strcmp(observation->value_normalized, "192.0.2.10") == 0 &&
strcmp(observation->role, "smtp_relay") == 0 &&
strcmp(observation->source_header, "received") == 0)
found_received_ip = TRUE;
}
assert(found_from && found_received_ip);
eml_analysis_free(analysis);
assert(g_remove(path) == 0); assert(g_rmdir(directory) == 0);
g_free(path); g_free(directory);
}
static void test_eml_analyzer_manual_fixture_regression(void)
{
GError *error = NULL;
EmlAnalysis *analysis = eml_analyzer_analyze_file(
"tests/fixtures/eml/manual_smoke_test.eml", &error);
assert(analysis != NULL && error == NULL);
const GPtrArray *domains = eml_analysis_get_domains(analysis);
for (guint index = 0; index < domains->len; index++)
{
const char *domain = g_ptr_array_index((GPtrArray *) domains, index);
assert(strcmp(domain, "192.0.2.10") != 0);
assert(strcmp(domain, "198.51.100.20") != 0);
assert(strcmp(domain, "1.0") != 0);
}
const GPtrArray *observations = eml_analysis_get_observations(analysis);
assert(observations != NULL && observations->len >= 10);
eml_analysis_free(analysis);
}
int main(void)
{
test_eml_analyzer_headers();
test_eml_analyzer_manual_fixture_regression();
puts("EmlAnalyzer : tous les tests sont valides."); return 0;
}

View file

@ -1,202 +0,0 @@
#include "core/eml_integration.h"
#include "dao/evidence_entity_dao.h"
#include "database/database.h"
#include "database/statement.h"
#include <assert.h>
#include <glib.h>
#include <glib/gstdio.h>
#include <stdio.h>
static void execute_done(Database *database, const char *sql)
{
DatabaseStatement *statement = database_statement_prepare(database, sql);
assert(statement != NULL);
assert(database_statement_step(statement) == DATABASE_STATEMENT_STEP_DONE);
database_statement_finalize(statement);
}
static gint64 count_rows(Database *database, const char *table)
{
char *sql = g_strdup_printf("SELECT COUNT(*) FROM %s;", table);
DatabaseStatement *statement = database_statement_prepare(database, sql);
int64_t count = -1;
g_free(sql);
assert(statement != NULL);
assert(database_statement_step(statement) == DATABASE_STATEMENT_STEP_ROW);
assert(database_statement_column_int64(statement, 0, &count));
database_statement_finalize(statement);
return count;
}
static char *read_observation_identifier(Database *database, const char *role)
{
DatabaseStatement *statement = database_statement_prepare(database,
"SELECT id FROM evidence_entity_observations WHERE role=?;");
char *identifier = NULL;
assert(statement != NULL && database_statement_bind_text(statement, 1, role));
assert(database_statement_step(statement) == DATABASE_STATEMENT_STEP_ROW);
assert(database_statement_column_text(statement, 0, &identifier));
database_statement_finalize(statement);
return identifier;
}
static char *read_observation_entity_identifier(
Database *database, const char *observation_identifier)
{
DatabaseStatement *statement = database_statement_prepare(database,
"SELECT entity_id FROM evidence_entity_observations WHERE id=?;");
char *identifier = NULL;
assert(statement != NULL &&
database_statement_bind_text(statement, 1, observation_identifier));
assert(database_statement_step(statement) == DATABASE_STATEMENT_STEP_ROW);
assert(database_statement_column_text(statement, 0, &identifier));
database_statement_finalize(statement);
return identifier;
}
int main(void)
{
GError *error = NULL;
char *directory = g_dir_make_tmp("labfy-eml-integration-XXXXXX", &error);
char *path = g_build_filename(directory, "Enquete.sqlite", NULL);
const char *evidence_id = "10000000-0000-4000-8000-000000000099";
assert(directory != NULL && error == NULL);
assert(database_initialize(path, "Synthétique", directory));
Database *database = database_open(path);
assert(database != NULL);
execute_done(database,
"INSERT INTO preuves(id,name,relative_path,type_id,size_bytes,sha256,"
"imported_at,updated_at,status,locked,original_name) VALUES("
"'10000000-0000-4000-8000-000000000099','fixture.eml',"
"'01_Preuves_Originales/fixture.eml',5,1,"
"'0000000000000000000000000000000000000000000000000000000000000000',"
"'2026-07-28T08:00:00Z','2026-07-28T08:00:00Z','active',0,"
"'fixture.eml');");
GPtrArray *proposals = g_ptr_array_new_with_free_func(
(GDestroyNotify) eml_entity_proposal_free);
EmlEntityProposal *proposal = eml_entity_proposal_new_observation(
"email_address", "Sender@Example.test", "sender@example.test",
"from", "from", 1, "confirmed", "header");
g_ptr_array_add(proposals, proposal);
guint observations = 0, created = 0, reused = 0;
assert(eml_integration_apply(database, evidence_id, proposals,
&observations, &created, &reused, &error));
assert(error == NULL && observations == 1 && created == 0 && reused == 0);
assert(count_rows(database, "evidence_entity_observations") == 1);
assert(count_rows(database, "entites") == 0);
assert(count_rows(database, "preuve_entites") == 0);
assert(eml_integration_apply(database, evidence_id, proposals,
&observations, &created, &reused, &error));
assert(count_rows(database, "evidence_entity_observations") == 1);
assert(count_rows(database, "entites") == 0);
proposal->promote_to_entity = TRUE;
assert(eml_integration_apply(database, evidence_id, proposals,
&observations, &created, &reused, &error));
assert(created == 1 && count_rows(database, "entites") == 1);
assert(count_rows(database, "preuve_entites") == 1);
assert(eml_integration_apply(database, evidence_id, proposals,
&observations, &created, &reused, &error));
assert(created == 0 && reused == 1 && count_rows(database, "entites") == 1);
char *from_observation = read_observation_identifier(database, "from");
char *from_entity = read_observation_entity_identifier(
database, from_observation);
EvidenceEntityDao *link_dao = evidence_entity_dao_new(database, &error);
assert(link_dao != NULL && error == NULL);
/* Vérifie aussi la coexistence des propriétaires manuel et EML. */
assert(evidence_entity_dao_link(link_dao, evidence_id, from_entity, &error));
gboolean deleted = FALSE, shared = FALSE;
assert(eml_integration_remove_promotion(database, from_observation,
&deleted, &shared, &error));
assert(!deleted && shared);
assert(count_rows(database, "evidence_entity_observations") == 1);
assert(count_rows(database, "entites") == 1);
assert(count_rows(database, "preuve_entites") == 1);
assert(evidence_entity_dao_unlink(link_dao, evidence_id, from_entity,
&error));
assert(count_rows(database, "preuve_entites") == 0);
execute_done(database, "DELETE FROM entites;");
evidence_entity_dao_free(link_dao);
g_free(from_entity);
assert(eml_integration_apply(database, evidence_id, proposals,
&observations, &created, &reused, &error));
assert(created == 1);
EmlEntityProposal *second = eml_entity_proposal_new_observation(
"email_address", "Sender@Example.test", "sender@example.test",
"reply_to", "reply-to", 1, "confirmed", "header");
second->promote_to_entity = TRUE;
g_ptr_array_add(proposals, second);
assert(eml_integration_apply(database, evidence_id, proposals,
&observations, &created, &reused, &error));
assert(count_rows(database, "evidence_entity_observations") == 2);
assert(count_rows(database, "entites") == 1);
assert(eml_integration_remove_promotion(database, from_observation,
&deleted, &shared, &error));
assert(!deleted && shared);
assert(count_rows(database, "evidence_entity_observations") == 2);
assert(count_rows(database, "entites") == 1);
assert(count_rows(database, "preuve_entites") == 1);
execute_done(database,
"INSERT INTO entites(id,type_id,valeur,label,description,confiance,"
"created_at,updated_at,status) VALUES("
"'20000000-0000-4000-8000-000000000099',1,'other@example.test',"
"'other@example.test',NULL,50,'2026-07-28T08:00:00Z',"
"'2026-07-28T08:00:00Z','active');");
execute_done(database,
"INSERT INTO relations(id,entite_source_id,entite_cible_id,"
"type_relation,label,justification,confiance,created_at,updated_at,"
"status,relation_type_id) SELECT "
"'30000000-0000-4000-8000-000000000099',o.entity_id,"
"'20000000-0000-4000-8000-000000000099','supports',NULL,NULL,50,"
"'2026-07-28T08:00:00Z','2026-07-28T08:00:00Z','active',rt.id "
"FROM evidence_entity_observations o,relation_types rt "
"WHERE o.role='reply_to' AND rt.code='supports';");
char *reply_observation = read_observation_identifier(database, "reply_to");
assert(eml_integration_remove_promotion(database, reply_observation,
&deleted, &shared, &error));
assert(!deleted && shared);
assert(count_rows(database, "relations") == 1);
assert(count_rows(database, "evidence_entity_observations") == 2);
/* Reproduction exacte : rattachement manuel avant la promotion EML. */
execute_done(database,
"INSERT INTO entites(id,type_id,valeur,label,description,confiance,"
"created_at,updated_at,status) VALUES("
"'20000000-0000-4000-8000-000000000107',1,'manual@example.test',"
"'manual@example.test',NULL,50,'2026-07-28T08:00:00Z',"
"'2026-07-28T08:00:00Z','active');");
link_dao = evidence_entity_dao_new(database, &error);
assert(link_dao != NULL &&
evidence_entity_dao_link(link_dao, evidence_id,
"20000000-0000-4000-8000-000000000107", &error));
GPtrArray *manual_first = g_ptr_array_new_with_free_func(
(GDestroyNotify) eml_entity_proposal_free);
EmlEntityProposal *manual_first_proposal =
eml_entity_proposal_new_observation("email_address",
"manual@example.test", "manual@example.test", "to", "to", 1,
"confirmed", "header");
manual_first_proposal->promote_to_entity = TRUE;
g_ptr_array_add(manual_first, manual_first_proposal);
assert(eml_integration_apply(database, evidence_id, manual_first,
&observations, &created, &reused, &error));
assert(created == 0 && reused == 1);
char *manual_first_observation = read_observation_identifier(database, "to");
assert(eml_integration_remove_promotion(database, manual_first_observation,
&deleted, &shared, &error));
gboolean manual_link_exists = FALSE;
assert(!deleted && shared &&
evidence_entity_dao_exists(link_dao, evidence_id,
"20000000-0000-4000-8000-000000000107", &manual_link_exists,
&error));
assert(manual_link_exists);
evidence_entity_dao_free(link_dao);
g_free(manual_first_observation);
g_ptr_array_unref(manual_first);
g_free(reply_observation);
g_free(from_observation);
g_ptr_array_unref(proposals);
database_close(database);
assert(g_remove(path) == 0 && g_rmdir(directory) == 0);
g_free(path); g_free(directory);
puts("EmlIntegration : tous les tests sont valides.");
return 0;
}

View file

@ -1,736 +0,0 @@
/******************************************************************************
* @file test_eml_mime_extractor.c
* @brief Tests synthétiques de l'extracteur MIME récursif.
******************************************************************************/
#include "core/eml_mime_extractor.h"
#include <glib.h>
#include <glib/gstdio.h>
typedef struct
{
char *directory;
char *eml_path;
char *output_directory;
} MimeFixture;
static MimeFixture *mime_fixture_new(const char *content)
{
GError *error = NULL;
MimeFixture *fixture = g_new0(MimeFixture, 1);
fixture->directory = g_dir_make_tmp("labfy-mime-XXXXXX", &error);
g_assert_no_error(error);
fixture->eml_path = g_build_filename(
fixture->directory,
"synthetic.eml",
NULL
);
fixture->output_directory = g_build_filename(
fixture->directory,
"derived",
NULL
);
g_assert_true(g_file_set_contents(
fixture->eml_path,
content,
-1,
&error
));
g_assert_no_error(error);
return fixture;
}
static void mime_fixture_free(MimeFixture *fixture)
{
GDir *directory = g_dir_open(fixture->output_directory, 0, NULL);
if (directory != NULL)
{
const char *name = NULL;
while ((name = g_dir_read_name(directory)) != NULL)
{
char *path = g_build_filename(
fixture->output_directory,
name,
NULL
);
g_assert_cmpint(g_remove(path), ==, 0);
g_free(path);
}
g_dir_close(directory);
g_assert_cmpint(g_rmdir(fixture->output_directory), ==, 0);
}
g_assert_cmpint(g_remove(fixture->eml_path), ==, 0);
g_assert_cmpint(g_rmdir(fixture->directory), ==, 0);
g_free(fixture->output_directory);
g_free(fixture->eml_path);
g_free(fixture->directory);
g_free(fixture);
}
static char *attachment_contents(EmlAttachment *attachment)
{
char *contents = NULL;
GError *error = NULL;
g_assert_true(g_file_get_contents(
attachment->extracted_path,
&contents,
NULL,
&error
));
g_assert_no_error(error);
return contents;
}
static void test_nested_order_and_encodings(void)
{
static const char eml[] =
"MIME-Version: 1.0\r\n"
"Content-Type: multipart/mixed; boundary=outer\r\n\r\n"
"--outer\r\nContent-Type: text/plain\r\n\r\nbody\r\n"
"--outer\r\n"
"Content-Type: multipart/related; boundary=inner\r\n\r\n"
"--inner\r\nContent-Type: image/png\r\n"
"Content-Disposition: inline\r\n"
"Content-ID: <synthetic-image@test.invalid>\r\n"
"Content-Transfer-Encoding: base64\r\n\r\n"
"UE5H\r\n"
"--inner\r\nContent-Type: text/plain\r\n"
"Content-Disposition: attachment;\r\n"
" filename*0*=UTF-8''rapport%20;\r\n"
" filename*1*=synth%C3%A9tique.txt\r\n"
"Content-Transfer-Encoding: quoted-printable\r\n\r\n"
"ligne=20une=\r\nligne=20deux\r\n"
"--inner--\r\n"
"--outer\r\nContent-Type: text/plain\r\n"
"Content-Disposition: attachment;\r\n"
" filename=\"=?UTF-8?Q?troisi=C3=A8me.txt?=\"\r\n\r\n"
"third\r\n--outer--\r\n";
MimeFixture *fixture = mime_fixture_new(eml);
GError *error = NULL;
EmlMimeResult *result = eml_mime_extract_attachments(
fixture->eml_path,
fixture->output_directory,
&error
);
g_assert_no_error(error);
g_assert_nonnull(result);
g_assert_cmpuint(result->attachments->len, ==, 3);
EmlAttachment *first = g_ptr_array_index(result->attachments, 0);
EmlAttachment *second = g_ptr_array_index(result->attachments, 1);
EmlAttachment *third = g_ptr_array_index(result->attachments, 2);
g_assert_cmpstr(first->part_index, ==, "1.2.1");
g_assert_true(first->is_inline);
g_assert_false(first->is_attachment);
g_assert_cmpstr(first->normalized_content_id, ==,
"synthetic-image@test.invalid");
g_assert_cmpstr(second->part_index, ==, "1.2.2");
g_assert_cmpstr(second->sanitized_filename, ==,
"rapport synthétique.txt");
g_assert_cmpstr(third->part_index, ==, "1.3");
g_assert_cmpstr(third->sanitized_filename, ==, "troisième.txt");
char *first_content = attachment_contents(first);
char *second_content = attachment_contents(second);
char *third_content = attachment_contents(third);
g_assert_cmpstr(first_content, ==, "PNG");
g_assert_cmpstr(second_content, ==, "ligne uneligne deux");
g_assert_cmpstr(third_content, ==, "third");
g_assert_nonnull(first->sha256);
g_assert_nonnull(first->detected_mime);
g_free(first_content);
g_free(second_content);
g_free(third_content);
eml_mime_result_free(result);
mime_fixture_free(fixture);
}
static void test_three_levels_and_message(void)
{
static const char eml[] =
"Content-Type: multipart/mixed; boundary=a\r\n\r\n"
"--a\r\nContent-Type: multipart/alternative; boundary=b\r\n\r\n"
"--b\r\nContent-Type: multipart/related; boundary=c\r\n\r\n"
"--c\r\nContent-Type: text/plain; name=four.txt\r\n"
"Content-Disposition: attachment\r\n\r\nfour\r\n--c--\r\n"
"--b--\r\n--a\r\nContent-Type: message/rfc822\r\n\r\n"
"Content-Type: text/plain; name=inside.txt\r\n"
"Content-Disposition: attachment\r\n\r\ninside\r\n"
"--a--\r\n";
MimeFixture *fixture = mime_fixture_new(eml);
GError *error = NULL;
EmlMimeResult *result = eml_mime_extract_attachments(
fixture->eml_path,
fixture->output_directory,
&error
);
g_assert_no_error(error);
g_assert_cmpuint(result->attachments->len, ==, 2);
EmlAttachment *first = g_ptr_array_index(result->attachments, 0);
EmlAttachment *second = g_ptr_array_index(result->attachments, 1);
g_assert_cmpstr(first->part_index, ==, "1.1.1.1");
g_assert_cmpstr(second->part_index, ==, "1.2.1");
eml_mime_result_free(result);
mime_fixture_free(fixture);
}
static void test_rfc2047_and_rfc2231_priorities(void)
{
static const char eml[] =
"Content-Type: multipart/mixed; boundary=x\r\n\r\n"
"--x\r\nContent-Type: text/plain; name=fallback.txt\r\n"
"Content-Disposition: attachment; filename=plain.txt;\r\n"
" filename*=ISO-8859-1''caf%E9.txt\r\n\r\none\r\n"
"--x\r\nContent-Type: text/plain\r\n"
"Content-Disposition: attachment;\r\n"
" filename=\"ASCII =?UTF-8?B?w6l0dWRl?=.txt\"\r\n\r\ntwo\r\n"
"--x\r\nContent-Type: text/plain\r\n"
"Content-Disposition: attachment;\r\n"
" filename*0=continued-; filename*1=name.txt\r\n\r\nthree\r\n"
"--x\r\nContent-Type: text/plain;\r\n"
" name*=UTF-8''type%20fallback.txt\r\n"
"Content-Disposition: inline\r\n\r\nfour\r\n"
"--x--\r\n";
MimeFixture *fixture = mime_fixture_new(eml);
GError *error = NULL;
EmlMimeResult *result = eml_mime_extract_attachments(
fixture->eml_path,
fixture->output_directory,
&error
);
g_assert_no_error(error);
g_assert_cmpuint(result->attachments->len, ==, 4);
g_assert_cmpstr(
((EmlAttachment *) g_ptr_array_index(
result->attachments, 0))->sanitized_filename,
==,
"café.txt"
);
g_assert_cmpstr(
((EmlAttachment *) g_ptr_array_index(
result->attachments, 1))->sanitized_filename,
==,
"ASCII étude.txt"
);
g_assert_cmpstr(
((EmlAttachment *) g_ptr_array_index(
result->attachments, 2))->sanitized_filename,
==,
"continued-name.txt"
);
g_assert_cmpstr(
((EmlAttachment *) g_ptr_array_index(
result->attachments, 3))->sanitized_filename,
==,
"type fallback.txt"
);
eml_mime_result_free(result);
mime_fixture_free(fixture);
}
static void test_invalid_encodings_are_warnings(void)
{
static const char eml[] =
"Content-Type: multipart/mixed; boundary=x\r\n\r\n"
"--x\r\nContent-Type: text/plain; name=a.txt\r\n"
"Content-Disposition: attachment\r\n"
"Content-Transfer-Encoding: base64\r\n\r\nA!AA\r\n"
"--x\r\nContent-Type: text/plain; name=b.txt\r\n"
"Content-Disposition: attachment\r\n"
"Content-Transfer-Encoding: quoted-printable\r\n\r\nbad=QZ\r\n"
"--x\r\nContent-Type: text/plain; name=c.txt\r\n"
"Content-Disposition: attachment\r\n"
"Content-Transfer-Encoding: synthetic\r\n\r\nbad\r\n"
"--x--\r\n";
MimeFixture *fixture = mime_fixture_new(eml);
GError *error = NULL;
EmlMimeResult *result = eml_mime_extract_attachments(
fixture->eml_path,
fixture->output_directory,
&error
);
g_assert_no_error(error);
g_assert_cmpuint(result->attachments->len, ==, 0);
g_assert_cmpuint(result->warnings->len, ==, 3);
eml_mime_result_free(result);
mime_fixture_free(fixture);
}
static void test_paths_and_collisions(void)
{
static const char eml[] =
"Content-Type: multipart/mixed; boundary=x\r\n\r\n"
"--x\r\nContent-Type: text/plain\r\n"
"Content-Disposition: attachment; filename=\"../same.txt\"\r\n\r\n1\r\n"
"--x\r\nContent-Type: text/plain\r\n"
"Content-Disposition: attachment; filename=\"C:\\\\same.txt\"\r\n\r\n2\r\n"
"--x\r\nContent-Type: text/plain\r\n"
"Content-Disposition: attachment; filename=\"/same.txt\"\r\n\r\n3\r\n"
"--x\r\nContent-Type: text/plain\r\n"
"Content-Disposition: attachment; filename=\"../same.txt\"\r\n\r\n4\r\n"
"--x--\r\n";
MimeFixture *fixture = mime_fixture_new(eml);
GError *error = NULL;
g_assert_cmpint(g_mkdir_with_parents(
fixture->output_directory, 0755), ==, 0);
char *existing = g_build_filename(
fixture->output_directory,
"___same.txt",
NULL
);
g_assert_true(g_file_set_contents(existing, "existing", -1, &error));
g_assert_no_error(error);
EmlMimeResult *result = eml_mime_extract_attachments(
fixture->eml_path,
fixture->output_directory,
&error
);
g_assert_no_error(error);
g_assert_cmpuint(result->attachments->len, ==, 4);
for (guint index = 0; index < result->attachments->len; index++)
{
EmlAttachment *attachment = g_ptr_array_index(
result->attachments,
index
);
g_assert_null(strchr(attachment->sanitized_filename, '/'));
g_assert_null(strchr(attachment->sanitized_filename, '\\'));
}
char *existing_content = NULL;
g_assert_true(g_file_get_contents(
existing, &existing_content, NULL, &error));
g_assert_no_error(error);
g_assert_cmpstr(existing_content, ==, "existing");
g_free(existing_content);
g_free(existing);
eml_mime_result_free(result);
mime_fixture_free(fixture);
}
static void test_malformed_and_incomplete_rfc2231(void)
{
static const char eml[] =
"Content-Type: multipart/mixed; boundary=x\r\n\r\n"
"--x\r\nContent-Type: text/plain\r\n"
"Content-Disposition: attachment; filename*0*=UTF-8''bad;\r\n"
" filename*2*=gap.txt; filename=fallback.txt\r\n\r\nok\r\n"
"--x\r\nContent-Type: text/plain\r\n"
"Content-Disposition: attachment; filename*0*=UTF-8''one;\r\n"
" filename*0*=duplicate; filename=duplicate-fallback.txt\r\n\r\ntwo\r\n";
MimeFixture *fixture = mime_fixture_new(eml);
GError *error = NULL;
EmlMimeResult *result = eml_mime_extract_attachments(
fixture->eml_path,
fixture->output_directory,
&error
);
g_assert_no_error(error);
g_assert_cmpuint(result->attachments->len, ==, 2);
EmlAttachment *attachment = g_ptr_array_index(result->attachments, 0);
g_assert_cmpstr(attachment->sanitized_filename, ==, "fallback.txt");
attachment = g_ptr_array_index(result->attachments, 1);
g_assert_cmpstr(
attachment->sanitized_filename,
==,
"duplicate-fallback.txt"
);
g_assert_cmpuint(result->warnings->len, >=, 1);
eml_mime_result_free(result);
mime_fixture_free(fixture);
}
static void test_missing_boundary(void)
{
static const char eml[] =
"Content-Type: multipart/mixed\r\n\r\nnot structured";
MimeFixture *fixture = mime_fixture_new(eml);
GError *error = NULL;
EmlMimeResult *result = eml_mime_extract_attachments(
fixture->eml_path,
fixture->output_directory,
&error
);
g_assert_no_error(error);
g_assert_cmpuint(result->attachments->len, ==, 0);
g_assert_cmpuint(result->warnings->len, ==, 1);
eml_mime_result_free(result);
mime_fixture_free(fixture);
}
static void test_cancelled_before_extraction(void)
{
static const char eml[] =
"Content-Type: text/plain; name=a.txt\r\n"
"Content-Disposition: attachment\r\n\r\ncontent";
MimeFixture *fixture = mime_fixture_new(eml);
GCancellable *cancellable = g_cancellable_new();
GError *error = NULL;
g_cancellable_cancel(cancellable);
EmlMimeResult *result = eml_mime_extract_attachments_cancellable(
fixture->eml_path,
fixture->output_directory,
cancellable,
&error
);
g_assert_null(result);
g_assert_error(error, G_IO_ERROR, G_IO_ERROR_CANCELLED);
g_clear_error(&error);
g_object_unref(cancellable);
mime_fixture_free(fixture);
}
static void test_source_unchanged(void)
{
static const char eml[] =
"Content-Type: text/plain; name=a.txt\r\n"
"Content-Disposition: attachment\r\n\r\nimmutable";
MimeFixture *fixture = mime_fixture_new(eml);
char *before = NULL;
char *after = NULL;
GError *error = NULL;
g_assert_true(g_file_get_contents(
fixture->eml_path, &before, NULL, &error));
EmlMimeResult *result = eml_mime_extract_attachments(
fixture->eml_path,
fixture->output_directory,
&error
);
g_assert_no_error(error);
g_assert_true(g_file_get_contents(
fixture->eml_path, &after, NULL, &error));
g_assert_no_error(error);
g_assert_cmpstr(before, ==, after);
g_free(before);
g_free(after);
eml_mime_result_free(result);
mime_fixture_free(fixture);
}
static void test_filename_sanitizer_limits(void)
{
char *empty = eml_mime_sanitize_filename(" ");
char *unix_path = eml_mime_sanitize_filename("../../absolute/test");
char *windows_path = eml_mime_sanitize_filename("C:\\temp\\test");
char *long_name = g_strnfill(
EML_MIME_MAX_FILENAME_LENGTH + 100,
'a'
);
char *shortened = eml_mime_sanitize_filename(long_name);
g_assert_cmpstr(empty, ==, "attachment.bin");
g_assert_null(strchr(unix_path, '/'));
g_assert_null(strchr(windows_path, '\\'));
g_assert_cmpuint(
strlen(shortened),
<=,
EML_MIME_MAX_FILENAME_LENGTH
);
g_free(empty);
g_free(unix_path);
g_free(windows_path);
g_free(long_name);
g_free(shortened);
}
static void test_part_count_limit(void)
{
GString *eml = g_string_new(
"Content-Type: multipart/mixed; boundary=x\r\n\r\n"
);
for (guint index = 0; index < EML_MIME_MAX_PARTS + 4; index++)
g_string_append_printf(
eml,
"--x\r\nContent-Type: text/plain; name=p%u.txt\r\n"
"Content-Disposition: attachment\r\n\r\n%u\r\n",
index,
index
);
g_string_append(eml, "--x--\r\n");
MimeFixture *fixture = mime_fixture_new(eml->str);
GError *error = NULL;
EmlMimeResult *result = eml_mime_extract_attachments(
fixture->eml_path,
fixture->output_directory,
&error
);
g_assert_no_error(error);
g_assert_cmpuint(
result->attachments->len,
==,
EML_MIME_MAX_PARTS - 1
);
g_assert_cmpuint(result->warnings->len, >, 0);
eml_mime_result_free(result);
mime_fixture_free(fixture);
g_string_free(eml, TRUE);
}
static void test_depth_limit(void)
{
GString *eml = g_string_new(NULL);
for (guint depth = 1; depth <= EML_MIME_MAX_DEPTH + 1; depth++)
g_string_append_printf(
eml,
"Content-Type: multipart/mixed; boundary=b%u\r\n\r\n--b%u\r\n",
depth,
depth
);
g_string_append(
eml,
"Content-Type: text/plain; name=too-deep.txt\r\n"
"Content-Disposition: attachment\r\n\r\ndeep\r\n"
);
for (gint depth = (gint) EML_MIME_MAX_DEPTH + 1; depth >= 1; depth--)
g_string_append_printf(eml, "--b%d--\r\n", depth);
MimeFixture *fixture = mime_fixture_new(eml->str);
GError *error = NULL;
EmlMimeResult *result = eml_mime_extract_attachments(
fixture->eml_path,
fixture->output_directory,
&error
);
g_assert_no_error(error);
g_assert_cmpuint(result->attachments->len, ==, 0);
g_assert_cmpuint(result->warnings->len, >, 0);
eml_mime_result_free(result);
mime_fixture_free(fixture);
g_string_free(eml, TRUE);
}
static void test_part_size_and_no_temporary_file(void)
{
GString *eml = g_string_new(
"Content-Type: text/plain; name=large.txt\r\n"
"Content-Disposition: attachment\r\n\r\n"
);
char *large_content = g_strnfill(
EML_MIME_MAX_PART_DECODED_SIZE + 1,
'x'
);
g_string_append_len(
eml,
large_content,
EML_MIME_MAX_PART_DECODED_SIZE + 1
);
g_free(large_content);
MimeFixture *fixture = mime_fixture_new(eml->str);
GError *error = NULL;
EmlMimeResult *result = eml_mime_extract_attachments(
fixture->eml_path,
fixture->output_directory,
&error
);
g_assert_no_error(error);
g_assert_cmpuint(result->attachments->len, ==, 0);
g_assert_cmpuint(result->warnings->len, ==, 1);
GDir *directory = g_dir_open(fixture->output_directory, 0, &error);
g_assert_no_error(error);
g_assert_null(g_dir_read_name(directory));
g_dir_close(directory);
eml_mime_result_free(result);
mime_fixture_free(fixture);
g_string_free(eml, TRUE);
}
static void test_empty_base64_and_malformed_header(void)
{
static const char valid[] =
"Content-Type: application/octet-stream; name=empty.bin\r\n"
"Content-Disposition: attachment\r\n"
"Content-Transfer-Encoding: base64\r\n\r\n";
MimeFixture *fixture = mime_fixture_new(valid);
GError *error = NULL;
EmlMimeResult *result = eml_mime_extract_attachments(
fixture->eml_path,
fixture->output_directory,
&error
);
g_assert_no_error(error);
g_assert_cmpuint(result->attachments->len, ==, 1);
EmlAttachment *attachment = g_ptr_array_index(result->attachments, 0);
g_assert_cmpuint(attachment->decoded_size, ==, 0);
eml_mime_result_free(result);
mime_fixture_free(fixture);
fixture = mime_fixture_new("Malformed header\r\n\r\nbody");
result = eml_mime_extract_attachments(
fixture->eml_path,
fixture->output_directory,
&error
);
g_assert_no_error(error);
g_assert_cmpuint(result->warnings->len, ==, 1);
eml_mime_result_free(result);
mime_fixture_free(fixture);
}
typedef struct
{
GCancellable *cancellable;
const char *output_directory;
} CancellationData;
static gpointer cancel_when_extraction_starts(gpointer user_data)
{
CancellationData *data = user_data;
while (!g_file_test(data->output_directory, G_FILE_TEST_IS_DIR))
g_thread_yield();
g_cancellable_cancel(data->cancellable);
return NULL;
}
static void test_cancelled_during_extraction(void)
{
GString *eml = g_string_new(
"Content-Type: text/plain; name=a.txt\r\n"
"Content-Disposition: attachment\r\n\r\n"
);
char *large_content = g_strnfill(
EML_MIME_MAX_PART_DECODED_SIZE,
'c'
);
g_string_append_len(
eml,
large_content,
EML_MIME_MAX_PART_DECODED_SIZE
);
g_free(large_content);
MimeFixture *fixture = mime_fixture_new(eml->str);
GCancellable *cancellable = g_cancellable_new();
CancellationData data = {
.cancellable = cancellable,
.output_directory = fixture->output_directory
};
GThread *thread = g_thread_new(
"mime-cancel",
cancel_when_extraction_starts,
&data
);
GError *error = NULL;
EmlMimeResult *result = eml_mime_extract_attachments_cancellable(
fixture->eml_path,
fixture->output_directory,
cancellable,
&error
);
g_thread_join(thread);
g_assert_null(result);
g_assert_error(error, G_IO_ERROR, G_IO_ERROR_CANCELLED);
g_clear_error(&error);
GDir *directory = g_dir_open(fixture->output_directory, 0, &error);
g_assert_no_error(error);
g_assert_null(g_dir_read_name(directory));
g_dir_close(directory);
g_object_unref(cancellable);
mime_fixture_free(fixture);
g_string_free(eml, TRUE);
}
static void test_total_decoded_limit(void)
{
const gsize part_length = 7U * 1024U * 1024U;
char *part_content = g_strnfill(part_length, 'z');
GString *eml = g_string_new(
"Content-Type: multipart/mixed; boundary=total\r\n\r\n"
);
for (guint index = 0; index < 5; index++)
{
g_string_append_printf(
eml,
"--total\r\nContent-Type: application/octet-stream; "
"name=large-%u.bin\r\n"
"Content-Disposition: attachment\r\n\r\n",
index
);
g_string_append_len(eml, part_content, (gssize) part_length);
g_string_append(eml, "\r\n");
}
g_string_append(eml, "--total--\r\n");
g_free(part_content);
MimeFixture *fixture = mime_fixture_new(eml->str);
GError *error = NULL;
EmlMimeResult *result = eml_mime_extract_attachments(
fixture->eml_path,
fixture->output_directory,
&error
);
g_assert_no_error(error);
g_assert_cmpuint(result->attachments->len, ==, 4);
g_assert_cmpuint(result->warnings->len, ==, 1);
eml_mime_result_free(result);
mime_fixture_free(fixture);
g_string_free(eml, TRUE);
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
g_test_add_func(
"/eml-mime/nested-order-encodings",
test_nested_order_and_encodings
);
g_test_add_func(
"/eml-mime/three-levels-message",
test_three_levels_and_message
);
g_test_add_func(
"/eml-mime/rfc2047-rfc2231-priorities",
test_rfc2047_and_rfc2231_priorities
);
g_test_add_func(
"/eml-mime/invalid-encodings",
test_invalid_encodings_are_warnings
);
g_test_add_func(
"/eml-mime/paths-collisions",
test_paths_and_collisions
);
g_test_add_func(
"/eml-mime/malformed-rfc2231",
test_malformed_and_incomplete_rfc2231
);
g_test_add_func(
"/eml-mime/missing-boundary",
test_missing_boundary
);
g_test_add_func(
"/eml-mime/cancelled-before",
test_cancelled_before_extraction
);
g_test_add_func(
"/eml-mime/source-unchanged",
test_source_unchanged
);
g_test_add_func(
"/eml-mime/filename-sanitizer-limits",
test_filename_sanitizer_limits
);
g_test_add_func(
"/eml-mime/part-count-limit",
test_part_count_limit
);
g_test_add_func(
"/eml-mime/depth-limit",
test_depth_limit
);
g_test_add_func(
"/eml-mime/part-size-no-temporary",
test_part_size_and_no_temporary_file
);
g_test_add_func(
"/eml-mime/empty-base64-malformed-header",
test_empty_base64_and_malformed_header
);
g_test_add_func(
"/eml-mime/cancelled-during",
test_cancelled_during_extraction
);
g_test_add_func(
"/eml-mime/total-decoded-limit",
test_total_decoded_limit
);
return g_test_run();
}

View file

@ -68,9 +68,9 @@ static void test_eml_pipeline_basic(void)
EmlAttachment *att = g_ptr_array_index(result->mime_result->attachments, 0);
g_assert_cmpstr(att->sanitized_filename, ==, "___rib_suspect.txt");
att = g_ptr_array_index(result->mime_result->attachments, 1);
g_assert_cmpstr(att->sanitized_filename, ==, "___rib_suspect-2.txt");
g_assert_cmpstr(att->sanitized_filename, ==, "___rib_suspect.txt");
g_assert_true(g_str_has_suffix(att->extracted_path,
"/___rib_suspect-2.txt"));
"/1____rib_suspect.txt"));
g_assert_cmpuint(att->decoded_size, >, 0U);
/* Vérification de la détection de la proposition bancaire dans la pièce jointe */
@ -86,249 +86,9 @@ static void test_eml_pipeline_basic(void)
g_free(tmp_dir);
}
static void test_eml_pipeline_document_analysis(void)
{
GError *error = NULL;
char *tmp_dir = g_dir_make_tmp("labfy-eml-document-XXXXXX", &error);
g_assert_no_error(error);
char *eml_path = g_build_filename(tmp_dir, "document.eml", NULL);
char *processed_dir = g_build_filename(
tmp_dir, "02_Preuves_Traitees", NULL);
static const char eml[] =
"From: synthetic@example.test\r\n"
"Content-Type: multipart/mixed; boundary=x\r\n\r\n"
"--x\r\nContent-Type: image/png; name=synthetic.png\r\n"
"Content-Disposition: attachment; filename=synthetic.png\r\n"
"Content-Transfer-Encoding: base64\r\n\r\n"
"UE5H\r\n--x--\r\n";
g_assert_true(g_file_set_contents(eml_path, eml, -1, &error));
g_assert_no_error(error);
DocumentAnalysisTools tools = {
.exiftool = "tests/fake_document_tool",
.tesseract = "tests/fake_document_tool",
.pdfinfo = "tests/fake_document_tool",
.pdftotext = "tests/fake_document_tool",
.pdftoppm = "tests/fake_document_tool"
};
BackgroundTask *task = eml_pipeline_task_new_with_tools(
eml_path, processed_dir, "synthetic-evidence", &tools);
g_assert_nonnull(task);
while (background_task_get_state(task) ==
BACKGROUND_TASK_STATE_RUNNING ||
background_task_get_state(task) ==
BACKGROUND_TASK_STATE_PENDING)
g_main_context_iteration(NULL, TRUE);
g_assert_cmpint(background_task_get_state(task), ==,
BACKGROUND_TASK_STATE_COMPLETED);
EmlPipelineResult *result = background_task_get_result(task);
g_assert_nonnull(result);
g_assert_cmpuint(result->document_analyses->len, ==, 1);
DocumentFileAnalysis *document = g_ptr_array_index(
result->document_analyses, 0);
g_assert_nonnull(document->metadata);
g_assert_nonnull(document->ocr);
g_assert_cmpstr(document->ocr->text, ==,
"Texte OCR synthétique page une.\n");
background_task_unref(task);
g_remove(eml_path);
g_free(processed_dir);
g_free(eml_path);
g_free(tmp_dir);
}
static void wait_for_task(BackgroundTask *task)
{
while (background_task_get_state(task) ==
BACKGROUND_TASK_STATE_RUNNING ||
background_task_get_state(task) ==
BACKGROUND_TASK_STATE_PENDING)
g_main_context_iteration(NULL, TRUE);
}
static void remove_tree(const char *path)
{
GFile *directory = g_file_new_for_path(path);
GFileEnumerator *enumerator = g_file_enumerate_children(directory,
G_FILE_ATTRIBUTE_STANDARD_NAME "," G_FILE_ATTRIBUTE_STANDARD_TYPE,
G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, NULL);
if (enumerator != NULL)
{
GFileInfo *info = NULL;
while ((info = g_file_enumerator_next_file(
enumerator, NULL, NULL)) != NULL)
{
GFile *child = g_file_get_child(directory,
g_file_info_get_name(info));
char *child_path = g_file_get_path(child);
if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY)
remove_tree(child_path);
else
g_assert_true(g_file_delete(child, NULL, NULL));
g_free(child_path);
g_object_unref(child);
g_object_unref(info);
}
g_object_unref(enumerator);
}
g_assert_true(g_file_delete(directory, NULL, NULL));
g_object_unref(directory);
}
static DocumentAnalysisTools synthetic_tools(void)
{
DocumentAnalysisTools tools = {
.exiftool = "tests/fake_document_tool",
.tesseract = "tests/fake_document_tool",
.pdfinfo = "tests/fake_document_tool",
.pdftotext = "tests/fake_document_tool",
.pdftoppm = "tests/fake_document_tool"
};
return tools;
}
typedef struct
{
gboolean called;
BackgroundTaskState state;
} PipelineCompletion;
static void pipeline_completed(BackgroundTask *task, gpointer user_data)
{
PipelineCompletion *completion = user_data;
completion->called = TRUE;
completion->state = background_task_get_state(task);
}
static void test_manual_fixture_async_start(void)
{
GError *error = NULL;
char *source_before = NULL;
gsize source_length = 0;
g_assert_true(g_file_get_contents(
"tests/fixtures/eml/manual_smoke_test.eml",
&source_before, &source_length, &error));
g_assert_no_error(error);
char *staging = g_dir_make_tmp("labfy-eml-manual-XXXXXX", &error);
g_assert_no_error(error);
DocumentAnalysisTools tools = synthetic_tools();
PipelineCompletion completion = { 0 };
BackgroundTask *task = eml_pipeline_task_start(
"tests/fixtures/eml/manual_smoke_test.eml", staging,
"synthetic-evidence", &tools, pipeline_completed,
&completion, NULL);
g_assert_nonnull(task);
wait_for_task(task);
while (!completion.called)
g_main_context_iteration(NULL, TRUE);
g_assert_cmpint(completion.state, ==,
BACKGROUND_TASK_STATE_COMPLETED);
EmlPipelineResult *result = background_task_get_result(task);
g_assert_nonnull(result);
g_assert_cmpuint(result->mime_result->attachments->len, ==, 2);
g_assert_cmpuint(result->bank_proposals->len, ==, 1);
BankProposal *bank = g_ptr_array_index(result->bank_proposals, 0);
g_assert_false(bank->is_iban_valid);
g_assert_cmpstr(bank->normalized_iban, ==,
"FR0000000000000000000000000");
g_assert_cmpuint(
eml_analysis_get_header_values(result->analysis, "received")->len,
==, 2);
char *source_after = NULL;
gsize after_length = 0;
g_assert_true(g_file_get_contents(
"tests/fixtures/eml/manual_smoke_test.eml",
&source_after, &after_length, &error));
g_assert_no_error(error);
g_assert_cmpmem(source_after, after_length, source_before, source_length);
g_free(source_after);
g_free(source_before);
background_task_unref(task);
remove_tree(staging);
g_free(staging);
}
static void test_eml_pipeline_pdf_end_to_end_and_limit(void)
{
GError *error = NULL;
char *tmp_dir = g_dir_make_tmp("labfy-eml-pdf-XXXXXX", &error);
g_assert_no_error(error);
char *eml_path = g_build_filename(tmp_dir, "pdf.eml", NULL);
char *processed_dir = g_build_filename(tmp_dir, "processed", NULL);
static const char eml[] =
"From: synthetic@example.test\r\n"
"MIME-Version: 1.0\r\n"
"Content-Type: multipart/mixed; boundary=pdf-boundary\r\n\r\n"
"--pdf-boundary\r\n"
"Content-Type: application/pdf; name=scan.pdf\r\n"
"Content-Disposition: attachment; filename=scan.pdf\r\n"
"Content-Transfer-Encoding: base64\r\n\r\n"
"JVBERi1zeW50aGV0aWM=\r\n"
"--pdf-boundary\r\n"
"Content-Type: application/pdf; name=second.pdf\r\n"
"Content-Disposition: attachment; filename=second.pdf\r\n"
"Content-Transfer-Encoding: base64\r\n\r\n"
"JVBERi1zeW50aGV0aWM=\r\n"
"--pdf-boundary--\r\n";
g_assert_true(g_file_set_contents(eml_path, eml, -1, &error));
g_assert_no_error(error);
char *source_before = NULL;
gsize source_before_length = 0;
g_assert_true(g_file_get_contents(
eml_path, &source_before, &source_before_length, &error));
g_assert_no_error(error);
DocumentAnalysisTools tools = synthetic_tools();
BackgroundTask *task = eml_pipeline_task_new_with_tools_and_limit(
eml_path, processed_dir, "synthetic-evidence", &tools, 1);
g_assert_nonnull(task);
wait_for_task(task);
g_assert_cmpint(background_task_get_state(task), ==,
BACKGROUND_TASK_STATE_COMPLETED);
EmlPipelineResult *result = background_task_get_result(task);
g_assert_nonnull(result);
g_assert_cmpuint(result->mime_result->attachments->len, ==, 2);
g_assert_cmpuint(result->document_analyses->len, ==, 1);
g_assert_cmpuint(result->skipped_document_analyses, ==, 1);
g_assert_cmpint(result->state, ==, DOCUMENT_ANALYSIS_STATE_PARTIAL);
g_assert_cmpuint(result->warnings->len, ==, 1);
DocumentFileAnalysis *document = g_ptr_array_index(
result->document_analyses, 0);
g_assert_nonnull(document->pdf);
g_assert_false(document->pdf->native_text_usable);
g_assert_cmpuint(document->pdf->pages->len, ==, 2);
PdfPageAnalysis *first = g_ptr_array_index(document->pdf->pages, 0);
PdfPageAnalysis *second = g_ptr_array_index(document->pdf->pages, 1);
g_assert_cmpuint(first->page_number, ==, 1);
g_assert_cmpuint(second->page_number, ==, 2);
g_assert_nonnull(first->render_execution);
g_assert_nonnull(first->execution);
char *source_after = NULL;
gsize source_after_length = 0;
g_assert_true(g_file_get_contents(
eml_path, &source_after, &source_after_length, &error));
g_assert_no_error(error);
g_assert_cmpuint(source_after_length, ==, source_before_length);
g_assert_cmpmem(source_after, source_after_length,
source_before, source_before_length);
g_free(source_after);
g_free(source_before);
background_task_unref(task);
g_remove(eml_path);
g_free(processed_dir);
g_free(eml_path);
g_free(tmp_dir);
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
g_test_add_func("/eml-pipeline-task/basic", test_eml_pipeline_basic);
g_test_add_func("/eml-pipeline-task/document-analysis",
test_eml_pipeline_document_analysis);
g_test_add_func("/eml-pipeline-task/pdf-end-to-end-limit",
test_eml_pipeline_pdf_end_to_end_and_limit);
g_test_add_func("/eml-pipeline-task/manual-fixture-async",
test_manual_fixture_async_start);
return g_test_run();
}

View file

@ -523,7 +523,6 @@ static void test_evidence_entity_dao_link_valid(void)
GError *error =
NULL;
char *observation_identifier = NULL;
test_evidence_entity_dao_insert_evidence(
&fixture,
@ -566,30 +565,6 @@ static void test_evidence_entity_dao_link_valid(void)
assert(error == NULL);
assert(association_exists);
assert(evidence_entity_dao_add_observation(
fixture.evidence_entity_dao, evidence_identifier,
"email_address", "Contact@Example.org", "contact@example.org",
"from", "header", "from", 1, "confirmed",
"2026-07-28T08:00:00Z", &observation_identifier, &error));
assert(error == NULL);
assert(g_uuid_string_is_valid(observation_identifier));
g_clear_pointer(&observation_identifier, g_free);
/* Réapplication idempotente : ni entité ni observation dupliquée. */
assert(evidence_entity_dao_add_observation(
fixture.evidence_entity_dao, evidence_identifier,
"email_address", "Contact@Example.org", "contact@example.org",
"from", "header", "from", 1, "confirmed",
"2026-07-28T08:00:00Z", &observation_identifier, &error));
assert(g_uuid_string_is_valid(observation_identifier));
char *summary = evidence_entity_dao_format_observations(
fixture.evidence_entity_dao, evidence_identifier, &error);
assert(error == NULL && summary != NULL);
assert(strstr(summary, "contact@example.org") != NULL);
assert(strstr(summary, "rôle : from") != NULL);
assert(strstr(summary, "origine : from #1") != NULL);
assert(strchr(summary, '\n') == NULL);
g_free(summary);
g_free(observation_identifier);
test_evidence_entity_dao_fixture_clear(
&fixture
@ -597,7 +572,7 @@ static void test_evidence_entity_dao_link_valid(void)
}
/**
* @brief Vérifie l'idempotence d'une association manuelle.
* @brief Vérifie le refus d'une association dupliquée.
*/
static void test_evidence_entity_dao_link_duplicate(void)
{
@ -637,7 +612,7 @@ static void test_evidence_entity_dao_link_duplicate(void)
assert(error == NULL);
assert(
evidence_entity_dao_link(
!evidence_entity_dao_link(
fixture.evidence_entity_dao,
evidence_identifier,
entity_identifier,
@ -645,7 +620,10 @@ static void test_evidence_entity_dao_link_duplicate(void)
)
);
assert(error == NULL);
test_evidence_entity_dao_assert_error(
error,
EVIDENCE_ENTITY_DAO_ERROR_CONSTRAINT
);
assert(
test_evidence_entity_dao_count_rows(
@ -653,6 +631,10 @@ static void test_evidence_entity_dao_link_duplicate(void)
) == 1
);
g_clear_error(
&error
);
test_evidence_entity_dao_fixture_clear(
&fixture
);

View file

@ -1,130 +0,0 @@
/******************************************************************************
* @file test_exiftool_analysis.c
* @brief Tests synthétiques de l'analyse ExifTool.
******************************************************************************/
#include "core/exiftool_analysis.h"
#include <glib.h>
#include <glib/gstdio.h>
static void test_parse_and_sensitive_gps(void)
{
const char *json =
"[{\"File:MIMEType\":\"image/png\",\"File:FileSize\":42,"
"\"EXIF:ImageWidth\":10,\"EXIF:GPSLatitude\":48.5,"
"\"EXIF:GPSLongitude\":2.2,\"EXIF:Unknown\":true}]";
GError *error = NULL;
ExiftoolAnalysisResult *result = exiftool_analysis_parse(
"synthetic.png", json, "synthetic warning", 1, &error);
g_assert_no_error(error);
g_assert_nonnull(result);
g_assert_cmpint(result->execution->state, ==,
DOCUMENT_ANALYSIS_STATE_PARTIAL);
g_assert_cmpstr(result->execution->raw_stdout, ==, json);
g_assert_nonnull(result->execution->raw_stdout_sha256);
g_assert_cmpuint(result->metadata->len, ==, 6);
DocumentMetadataEntry *latitude =
g_ptr_array_index(result->metadata, 3);
g_assert_cmpstr(latitude->code, ==, "image.gps_latitude");
g_assert_true(latitude->sensitive);
g_assert_true(latitude->requires_confirmation);
DocumentMetadataEntry *unknown =
g_ptr_array_index(result->metadata, 5);
g_assert_cmpstr(unknown->code, ==, "metadata.unknown");
g_assert_cmpstr(unknown->original_tag, ==, "Unknown");
exiftool_analysis_result_free(result);
}
static void test_invalid_json(void)
{
GError *error = NULL;
g_assert_null(exiftool_analysis_parse(
"synthetic.png", "[{\"broken\":", NULL, 0, &error));
g_assert_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA);
g_clear_error(&error);
}
static void test_run_and_unavailable(void)
{
GError *error = NULL;
char *directory = g_dir_make_tmp("labfy-exif-XXXXXX", &error);
char *path = g_build_filename(directory, "synthetic.png", NULL);
g_assert_true(g_file_set_contents(path, "PNG", 3, &error));
ExiftoolAnalysisResult *result = exiftool_analysis_run(
"tests/fake_document_tool", path, NULL, &error);
g_assert_no_error(error);
g_assert_cmpint(result->execution->state, ==,
DOCUMENT_ANALYSIS_STATE_SUCCESS);
g_assert_cmpstr(result->execution->version, ==, "13.00");
exiftool_analysis_result_free(result);
result = exiftool_analysis_run(
"tests/missing_document_tool", path, NULL, &error);
g_assert_no_error(error);
g_assert_cmpint(result->execution->state, ==,
DOCUMENT_ANALYSIS_STATE_UNAVAILABLE);
exiftool_analysis_result_free(result);
g_remove(path);
g_rmdir(directory);
g_free(path);
g_free(directory);
}
static gpointer cancel_exiftool(gpointer user_data)
{
g_usleep(50000);
g_cancellable_cancel(user_data);
return NULL;
}
static void test_cancellation_and_truncated_json(void)
{
GError *error = NULL;
char *directory = g_dir_make_tmp("labfy-exif-hardening-XXXXXX", &error);
char *slow_path = g_build_filename(directory, "slow.png", NULL);
char *large_path = g_build_filename(directory, "large.png", NULL);
g_assert_true(g_file_set_contents(slow_path, "PNG", 3, &error));
g_assert_true(g_file_set_contents(large_path, "PNG", 3, &error));
GCancellable *cancellable = g_cancellable_new();
GThread *thread = g_thread_new(
"exif-cancel", cancel_exiftool, cancellable);
ExiftoolAnalysisResult *result = exiftool_analysis_run(
"tests/fake_document_tool", slow_path, cancellable, &error);
g_thread_join(thread);
g_assert_null(result);
g_assert_error(error, G_IO_ERROR, G_IO_ERROR_CANCELLED);
g_clear_error(&error);
g_object_unref(cancellable);
DocumentToolRunnerLimits limits = { 128, 128 };
result = exiftool_analysis_run_with_limits(
"tests/fake_document_tool", large_path, &limits, NULL, &error);
g_assert_no_error(error);
g_assert_nonnull(result);
g_assert_true(result->execution->stdout_truncated);
g_assert_cmpuint(strlen(result->execution->raw_stdout), ==, 128);
g_assert_cmpuint(result->metadata->len, ==, 0);
g_assert_cmpint(result->execution->state, ==,
DOCUMENT_ANALYSIS_STATE_FAILED);
g_assert_cmpuint(result->execution->warnings->len, >, 0);
exiftool_analysis_result_free(result);
g_remove(slow_path);
g_remove(large_path);
g_rmdir(directory);
g_free(slow_path);
g_free(large_path);
g_free(directory);
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
g_test_add_func("/exiftool-analysis/parse-gps",
test_parse_and_sensitive_gps);
g_test_add_func("/exiftool-analysis/invalid-json", test_invalid_json);
g_test_add_func("/exiftool-analysis/run-unavailable",
test_run_and_unavailable);
g_test_add_func("/exiftool-analysis/cancellation-truncated",
test_cancellation_and_truncated_json);
return g_test_run();
}

View file

@ -1,111 +0,0 @@
/******************************************************************************
* @file test_ocr_analysis.c
* @brief Tests synthétiques de l'OCR.
******************************************************************************/
#include "core/ocr_analysis.h"
#include <glib.h>
#include <glib/gstdio.h>
static void test_languages_and_raw_text(void)
{
GError *error = NULL;
char *directory = g_dir_make_tmp("labfy-ocr-XXXXXX", &error);
char *path = g_build_filename(directory, "synthetic.png", NULL);
g_assert_true(g_file_set_contents(path, "PNG", 3, &error));
const char *languages[] = { "fra", "eng", "fra+eng" };
for (guint index = 0; index < G_N_ELEMENTS(languages); index++)
{
OcrAnalysisResult *result = ocr_analysis_run(
"tests/fake_document_tool", path, languages[index],
NULL, &error);
g_assert_no_error(error);
g_assert_cmpstr(result->requested_languages, ==, languages[index]);
g_assert_cmpstr(result->text, ==,
"Texte OCR synthétique page une.\n");
g_assert_cmpstr(result->execution->raw_stdout, ==, result->text);
g_assert_nonnull(result->execution->version);
ocr_analysis_result_free(result);
}
g_remove(path);
g_rmdir(directory);
g_free(path);
g_free(directory);
}
static void test_unavailable_and_compatibility(void)
{
GError *error = NULL;
OcrAnalysisResult *result = ocr_analysis_run(
"tests/missing_document_tool", "synthetic.png", "fra",
NULL, &error);
g_assert_no_error(error);
g_assert_cmpint(result->execution->state, ==,
DOCUMENT_ANALYSIS_STATE_UNAVAILABLE);
ocr_analysis_result_free(result);
g_assert_true(ocr_analysis_mime_is_compatible("image/png"));
g_assert_true(ocr_analysis_mime_is_compatible("image/tiff"));
g_assert_false(ocr_analysis_mime_is_compatible("text/plain"));
}
static gpointer cancel_ocr(gpointer user_data)
{
g_usleep(50000);
g_cancellable_cancel(user_data);
return NULL;
}
static void test_cancellation(void)
{
GError *error = NULL;
char *directory = g_dir_make_tmp("labfy-ocr-XXXXXX", &error);
char *path = g_build_filename(directory, "sleep.png", NULL);
g_assert_true(g_file_set_contents(path, "PNG", 3, &error));
GCancellable *cancellable = g_cancellable_new();
GThread *thread = g_thread_new("ocr-cancel", cancel_ocr, cancellable);
OcrAnalysisResult *result = ocr_analysis_run(
"tests/fake_document_tool", path, "fra", cancellable, &error);
g_thread_join(thread);
g_assert_null(result);
g_assert_error(error, G_IO_ERROR, G_IO_ERROR_CANCELLED);
g_clear_error(&error);
g_object_unref(cancellable);
g_remove(path);
g_rmdir(directory);
g_free(path);
g_free(directory);
}
static void test_truncated_text(void)
{
GError *error = NULL;
char *directory = g_dir_make_tmp("labfy-ocr-limit-XXXXXX", &error);
char *path = g_build_filename(directory, "large.png", NULL);
g_assert_true(g_file_set_contents(path, "PNG", 3, &error));
DocumentToolRunnerLimits limits = { 96, 64 };
OcrAnalysisResult *result = ocr_analysis_run_with_limits(
"tests/fake_document_tool", path, "fra", &limits,
NULL, &error);
g_assert_no_error(error);
g_assert_nonnull(result);
g_assert_cmpuint(strlen(result->text), ==, 96);
g_assert_true(result->execution->stdout_truncated);
g_assert_cmpint(result->execution->state, ==,
DOCUMENT_ANALYSIS_STATE_PARTIAL);
ocr_analysis_result_free(result);
g_remove(path);
g_rmdir(directory);
g_free(path);
g_free(directory);
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
g_test_add_func("/ocr-analysis/languages-raw",
test_languages_and_raw_text);
g_test_add_func("/ocr-analysis/unavailable-compatible",
test_unavailable_and_compatibility);
g_test_add_func("/ocr-analysis/cancellation", test_cancellation);
g_test_add_func("/ocr-analysis/truncated-text", test_truncated_text);
return g_test_run();
}

View file

@ -1,156 +0,0 @@
/******************************************************************************
* @file test_pdf_analysis.c
* @brief Tests synthétiques de l'analyse PDF.
******************************************************************************/
#include "core/pdf_analysis.h"
#include <glib.h>
#include <glib/gstdio.h>
static PdfAnalysisTools fake_tools(void)
{
PdfAnalysisTools tools = {
.pdfinfo = "tests/fake_document_tool",
.pdftotext = "tests/fake_document_tool",
.pdftoppm = "tests/fake_document_tool",
.tesseract = "tests/fake_document_tool"
};
return tools;
}
static char *create_pdf(const char *directory, const char *name)
{
char *path = g_build_filename(directory, name, NULL);
g_assert_true(g_file_set_contents(path, "%PDF-synthetic", -1, NULL));
return path;
}
static void test_encrypted_and_native(void)
{
GError *error = NULL;
char *directory = g_dir_make_tmp("labfy-pdf-XXXXXX", &error);
PdfAnalysisTools tools = fake_tools();
char *encrypted = create_pdf(directory, "encrypted.pdf");
PdfAnalysisResult *result = pdf_analysis_run(
&tools, encrypted, "fra", NULL, &error);
g_assert_no_error(error);
g_assert_true(result->encrypted);
g_assert_cmpuint(result->pages->len, ==, 0);
pdf_analysis_result_free(result);
char *native = create_pdf(directory, "native.pdf");
result = pdf_analysis_run(&tools, native, "fra", NULL, &error);
g_assert_no_error(error);
g_assert_true(result->native_text_usable);
g_assert_cmpuint(result->pages->len, ==, 2);
PdfPageAnalysis *page = g_ptr_array_index(result->pages, 0);
g_assert_cmpint(page->method, ==, PDF_PAGE_METHOD_NATIVE);
pdf_analysis_result_free(result);
g_remove(encrypted);
g_remove(native);
g_rmdir(directory);
g_free(encrypted);
g_free(native);
g_free(directory);
}
static void test_ocr_fallback_order_and_cleanup(void)
{
GError *error = NULL;
char *directory = g_dir_make_tmp("labfy-pdf-XXXXXX", &error);
char *scan = create_pdf(directory, "scan.pdf");
PdfAnalysisTools tools = fake_tools();
PdfAnalysisResult *result = pdf_analysis_run(
&tools, scan, "fra+eng", NULL, &error);
g_assert_no_error(error);
g_assert_false(result->native_text_usable);
g_assert_cmpuint(result->pages->len, ==, 2);
PdfPageAnalysis *first = g_ptr_array_index(result->pages, 0);
PdfPageAnalysis *second = g_ptr_array_index(result->pages, 1);
g_assert_cmpuint(first->page_number, ==, 1);
g_assert_cmpuint(second->page_number, ==, 2);
g_assert_cmpint(first->method, ==, PDF_PAGE_METHOD_OCR);
g_assert_nonnull(strstr(second->text, "page deux"));
pdf_analysis_result_free(result);
g_remove(scan);
g_rmdir(directory);
g_free(scan);
g_free(directory);
}
static void test_heuristic(void)
{
g_assert_false(pdf_analysis_text_is_usable(""));
g_assert_false(pdf_analysis_text_is_usable("court"));
g_assert_true(pdf_analysis_text_is_usable(
"Texte synthétique imprimable et suffisamment long pour le test."));
}
static gpointer cancel_pdf(gpointer user_data)
{
g_usleep(70000);
g_cancellable_cancel(user_data);
return NULL;
}
static void assert_cancelled_pdf(
const PdfAnalysisTools *tools,
const char *path,
guint expected_completed_pages
)
{
GCancellable *cancellable = g_cancellable_new();
GThread *thread = g_thread_new("pdf-cancel", cancel_pdf, cancellable);
GError *error = NULL;
PdfAnalysisResult *result = pdf_analysis_run(
tools, path, "fra", cancellable, &error);
g_thread_join(thread);
g_assert_no_error(error);
g_assert_nonnull(result);
g_assert_cmpuint(result->pages->len, ==, expected_completed_pages);
g_assert_true(result->state == DOCUMENT_ANALYSIS_STATE_CANCELLED ||
result->state == DOCUMENT_ANALYSIS_STATE_PARTIAL);
pdf_analysis_result_free(result);
g_object_unref(cancellable);
}
static void test_cancellation_stages_and_completed_pages(void)
{
GError *error = NULL;
char *directory = g_dir_make_tmp("labfy-pdf-cancel-XXXXXX", &error);
g_assert_no_error(error);
PdfAnalysisTools tools = fake_tools();
char *slow_info = create_pdf(directory, "slow-info.pdf");
char *slow_text = create_pdf(directory, "slow-text.pdf");
char *slow_render = create_pdf(directory, "slow-render.pdf");
char *slow_ocr = create_pdf(directory, "slow-ocr.pdf");
char *slow_second = create_pdf(directory, "slow-page-2.pdf");
assert_cancelled_pdf(&tools, slow_info, 0);
assert_cancelled_pdf(&tools, slow_text, 0);
assert_cancelled_pdf(&tools, slow_render, 0);
assert_cancelled_pdf(&tools, slow_ocr, 1);
assert_cancelled_pdf(&tools, slow_second, 1);
g_remove(slow_info);
g_remove(slow_text);
g_remove(slow_render);
g_remove(slow_ocr);
g_remove(slow_second);
g_rmdir(directory);
g_free(slow_info);
g_free(slow_text);
g_free(slow_render);
g_free(slow_ocr);
g_free(slow_second);
g_free(directory);
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
g_test_add_func("/pdf-analysis/encrypted-native",
test_encrypted_and_native);
g_test_add_func("/pdf-analysis/ocr-fallback-cleanup",
test_ocr_fallback_order_and_cleanup);
g_test_add_func("/pdf-analysis/heuristic", test_heuristic);
g_test_add_func("/pdf-analysis/cancellation-stages",
test_cancellation_stages_and_completed_pages);
return g_test_run();
}