Compare commits
6 commits
8fcd6b0e0d
...
db9be54a1a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db9be54a1a | ||
|
|
d00d149bef | ||
|
|
a3c4b4c51a | ||
|
|
015841a20c | ||
|
|
d96fc83fc0 | ||
|
|
58fbf2daae |
64 changed files with 8347 additions and 583 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -17,3 +17,8 @@ labfy-investigation
|
||||||
.cache/
|
.cache/
|
||||||
compile_commands.json
|
compile_commands.json
|
||||||
/AGENTS.md
|
/AGENTS.md
|
||||||
|
/prompt.md
|
||||||
|
|
||||||
|
# OpenCode local
|
||||||
|
/opencode.json
|
||||||
|
/.opencode/
|
||||||
|
|
|
||||||
104
Makefile
104
Makefile
|
|
@ -126,6 +126,7 @@ TEST_SOCIAL_ACCOUNT_SERVICE := tests/test_social_account_service
|
||||||
TEST_SOCIAL_PLATFORM := tests/test_social_platform
|
TEST_SOCIAL_PLATFORM := tests/test_social_platform
|
||||||
TEST_PERSON_ENTITY_SERVICE := tests/test_person_entity_service
|
TEST_PERSON_ENTITY_SERVICE := tests/test_person_entity_service
|
||||||
TEST_EML_ANALYZER := tests/test_eml_analyzer
|
TEST_EML_ANALYZER := tests/test_eml_analyzer
|
||||||
|
TEST_EML_INTEGRATION := tests/test_eml_integration
|
||||||
TEST_IBAN_ANALYZER := tests/test_iban_analyzer
|
TEST_IBAN_ANALYZER := tests/test_iban_analyzer
|
||||||
TEST_EXIFTOOL_METADATA := tests/test_exiftool_metadata
|
TEST_EXIFTOOL_METADATA := tests/test_exiftool_metadata
|
||||||
TEST_PDF_PASSWORD_RECOVERY := tests/test_pdf_password_recovery
|
TEST_PDF_PASSWORD_RECOVERY := tests/test_pdf_password_recovery
|
||||||
|
|
@ -135,12 +136,31 @@ TEST_RELATION_TYPE_SERVICE := tests/test_relation_type_service
|
||||||
TEST_CONTROLLED_VOCAB := tests/test_controlled_vocab
|
TEST_CONTROLLED_VOCAB := tests/test_controlled_vocab
|
||||||
TEST_BANK_PROPOSAL := tests/test_bank_proposal
|
TEST_BANK_PROPOSAL := tests/test_bank_proposal
|
||||||
TEST_EML_PIPELINE_TASK := tests/test_eml_pipeline_task
|
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)
|
all: $(TARGET)
|
||||||
|
|
||||||
$(TEST_BANK_PROPOSAL): \
|
$(TEST_BANK_PROPOSAL): \
|
||||||
tests/test_bank_proposal.c \
|
tests/test_bank_proposal.c \
|
||||||
src/core/bank_proposal.c \
|
src/core/bank_proposal.c \
|
||||||
|
src/core/iban_analyzer.c \
|
||||||
src/core/controlled_vocab.c
|
src/core/controlled_vocab.c
|
||||||
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ \
|
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ \
|
||||||
$(shell $(PKG_CONFIG) --libs glib-2.0)
|
$(shell $(PKG_CONFIG) --libs glib-2.0)
|
||||||
|
|
@ -156,9 +176,52 @@ $(TEST_EML_PIPELINE_TASK): \
|
||||||
src/core/eml_pipeline_task.c src/core/eml_mime_extractor.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/eml_analyzer.c src/core/bank_proposal.c \
|
||||||
src/core/controlled_vocab.c src/core/iban_analyzer.c \
|
src/core/controlled_vocab.c src/core/iban_analyzer.c \
|
||||||
src/core/rib_ocr.c src/core/file_hash.c src/core/background_task.c
|
src/core/file_hash.c src/core/background_task.c \
|
||||||
$(CC) $(TEST_CFLAGS) -Wpedantic $^ -o $@ \
|
src/core/document_analysis.c src/core/document_tool_runner.c \
|
||||||
$(TEST_LDFLAGS) -lsqlite3
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -254,6 +317,7 @@ $(TEST_EXTRACTION_DROP_SERVICE): \
|
||||||
src/dao/entity_dao.c \
|
src/dao/entity_dao.c \
|
||||||
src/dao/evidence_entity_dao.c \
|
src/dao/evidence_entity_dao.c \
|
||||||
src/models/evidence_record.c \
|
src/models/evidence_record.c \
|
||||||
|
src/models/evidence_observation.c \
|
||||||
src/models/entity_record.c \
|
src/models/entity_record.c \
|
||||||
src/database/database.c \
|
src/database/database.c \
|
||||||
src/database/schema.c \
|
src/database/schema.c \
|
||||||
|
|
@ -513,6 +577,7 @@ $(TEST_EVIDENCE_ENTITY_DAO): \
|
||||||
src/dao/entity_dao.c \
|
src/dao/entity_dao.c \
|
||||||
src/models/evidence_record.c \
|
src/models/evidence_record.c \
|
||||||
src/models/entity_record.c \
|
src/models/entity_record.c \
|
||||||
|
src/models/evidence_observation.c \
|
||||||
src/database/database.c \
|
src/database/database.c \
|
||||||
src/database/schema.c \
|
src/database/schema.c \
|
||||||
src/database/statement.c \
|
src/database/statement.c \
|
||||||
|
|
@ -697,6 +762,7 @@ $(TEST_SOCIAL_ACCOUNT_SERVICE): \
|
||||||
src/dao/entity_dao.c \
|
src/dao/entity_dao.c \
|
||||||
src/dao/evidence_entity_dao.c \
|
src/dao/evidence_entity_dao.c \
|
||||||
src/models/entity_record.c \
|
src/models/entity_record.c \
|
||||||
|
src/models/evidence_observation.c \
|
||||||
src/database/database.c \
|
src/database/database.c \
|
||||||
src/database/schema.c \
|
src/database/schema.c \
|
||||||
src/database/statement.c \
|
src/database/statement.c \
|
||||||
|
|
@ -715,6 +781,7 @@ $(TEST_PERSON_ENTITY_SERVICE): \
|
||||||
src/dao/entity_dao.c \
|
src/dao/entity_dao.c \
|
||||||
src/dao/evidence_entity_dao.c \
|
src/dao/evidence_entity_dao.c \
|
||||||
src/models/entity_record.c \
|
src/models/entity_record.c \
|
||||||
|
src/models/evidence_observation.c \
|
||||||
src/database/database.c \
|
src/database/database.c \
|
||||||
src/database/schema.c \
|
src/database/schema.c \
|
||||||
src/database/statement.c \
|
src/database/statement.c \
|
||||||
|
|
@ -725,6 +792,14 @@ $(TEST_PERSON_ENTITY_SERVICE): \
|
||||||
$(TEST_EML_ANALYZER): tests/test_eml_analyzer.c src/core/eml_analyzer.c
|
$(TEST_EML_ANALYZER): tests/test_eml_analyzer.c src/core/eml_analyzer.c
|
||||||
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
|
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
|
||||||
|
|
||||||
|
$(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
|
$(TEST_IBAN_ANALYZER): tests/test_iban_analyzer.c src/core/iban_analyzer.c
|
||||||
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
|
$(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS)
|
||||||
|
|
||||||
|
|
@ -821,6 +896,7 @@ test: \
|
||||||
$(TEST_SOCIAL_PLATFORM) \
|
$(TEST_SOCIAL_PLATFORM) \
|
||||||
$(TEST_PERSON_ENTITY_SERVICE) \
|
$(TEST_PERSON_ENTITY_SERVICE) \
|
||||||
$(TEST_EML_ANALYZER) \
|
$(TEST_EML_ANALYZER) \
|
||||||
|
$(TEST_EML_INTEGRATION) \
|
||||||
$(TEST_IBAN_ANALYZER) \
|
$(TEST_IBAN_ANALYZER) \
|
||||||
$(TEST_EXIFTOOL_METADATA) \
|
$(TEST_EXIFTOOL_METADATA) \
|
||||||
$(TEST_PDF_PASSWORD_RECOVERY) \
|
$(TEST_PDF_PASSWORD_RECOVERY) \
|
||||||
|
|
@ -829,7 +905,12 @@ test: \
|
||||||
$(TEST_RELATION_TYPE_SERVICE) \
|
$(TEST_RELATION_TYPE_SERVICE) \
|
||||||
$(TEST_CONTROLLED_VOCAB) \
|
$(TEST_CONTROLLED_VOCAB) \
|
||||||
$(TEST_BANK_PROPOSAL) \
|
$(TEST_BANK_PROPOSAL) \
|
||||||
$(TEST_EML_PIPELINE_TASK)
|
$(TEST_EML_PIPELINE_TASK) \
|
||||||
|
$(TEST_EML_MIME_EXTRACTOR) \
|
||||||
|
$(TEST_EXIFTOOL_ANALYSIS) \
|
||||||
|
$(TEST_OCR_ANALYSIS) \
|
||||||
|
$(TEST_PDF_ANALYSIS) \
|
||||||
|
$(TEST_DOCUMENT_TOOL_RUNNER)
|
||||||
@echo "Exécution des tests..."
|
@echo "Exécution des tests..."
|
||||||
@./$(TEST_NODE)
|
@./$(TEST_NODE)
|
||||||
@./$(TEST_TREE_MODEL)
|
@./$(TEST_TREE_MODEL)
|
||||||
|
|
@ -889,6 +970,7 @@ test: \
|
||||||
@$(TEST_SOCIAL_PLATFORM)
|
@$(TEST_SOCIAL_PLATFORM)
|
||||||
@$(TEST_PERSON_ENTITY_SERVICE)
|
@$(TEST_PERSON_ENTITY_SERVICE)
|
||||||
@$(TEST_EML_ANALYZER)
|
@$(TEST_EML_ANALYZER)
|
||||||
|
@$(TEST_EML_INTEGRATION)
|
||||||
@$(TEST_IBAN_ANALYZER)
|
@$(TEST_IBAN_ANALYZER)
|
||||||
@$(TEST_EXIFTOOL_METADATA)
|
@$(TEST_EXIFTOOL_METADATA)
|
||||||
@$(TEST_PDF_PASSWORD_RECOVERY)
|
@$(TEST_PDF_PASSWORD_RECOVERY)
|
||||||
|
|
@ -898,6 +980,11 @@ test: \
|
||||||
@$(TEST_CONTROLLED_VOCAB)
|
@$(TEST_CONTROLLED_VOCAB)
|
||||||
@$(TEST_BANK_PROPOSAL)
|
@$(TEST_BANK_PROPOSAL)
|
||||||
@$(TEST_EML_PIPELINE_TASK)
|
@$(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."
|
@echo "Tous les tests sont valides."
|
||||||
|
|
||||||
%.o: %.c
|
%.o: %.c
|
||||||
|
|
@ -964,12 +1051,19 @@ clean:
|
||||||
$(TEST_SOCIAL_PLATFORM) \
|
$(TEST_SOCIAL_PLATFORM) \
|
||||||
$(TEST_PERSON_ENTITY_SERVICE) \
|
$(TEST_PERSON_ENTITY_SERVICE) \
|
||||||
$(TEST_EML_ANALYZER) \
|
$(TEST_EML_ANALYZER) \
|
||||||
|
$(TEST_EML_INTEGRATION) \
|
||||||
$(TEST_EXTRACTION_DROP_SERVICE) \
|
$(TEST_EXTRACTION_DROP_SERVICE) \
|
||||||
$(TEST_RELATION_TYPE_NORMALIZER) \
|
$(TEST_RELATION_TYPE_NORMALIZER) \
|
||||||
$(TEST_RELATION_TYPE_SERVICE) \
|
$(TEST_RELATION_TYPE_SERVICE) \
|
||||||
$(TEST_CONTROLLED_VOCAB) \
|
$(TEST_CONTROLLED_VOCAB) \
|
||||||
$(TEST_BANK_PROPOSAL) \
|
$(TEST_BANK_PROPOSAL) \
|
||||||
$(TEST_EML_PIPELINE_TASK)
|
$(TEST_EML_PIPELINE_TASK) \
|
||||||
|
$(TEST_EML_MIME_EXTRACTOR) \
|
||||||
|
$(TEST_EXIFTOOL_ANALYSIS) \
|
||||||
|
$(TEST_OCR_ANALYSIS) \
|
||||||
|
$(TEST_PDF_ANALYSIS) \
|
||||||
|
$(TEST_DOCUMENT_TOOL_RUNNER) \
|
||||||
|
$(FAKE_DOCUMENT_TOOL)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
39
README.md
39
README.md
|
|
@ -180,6 +180,37 @@ Le socle actuel comprend notamment :
|
||||||
- glisser-déposer des extractions texte depuis l'arborescence vers le graphe,
|
- glisser-déposer des extractions texte depuis l'arborescence vers le graphe,
|
||||||
avec confirmation explicite, création d'entité ou rattachement à une entité
|
avec confirmation explicite, création d'entité ou rattachement à une entité
|
||||||
existante, sans déplacement du fichier produit ;
|
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 :
|
Les outils actuellement présents dans le catalogue initial sont :
|
||||||
|
|
||||||
|
|
@ -285,7 +316,7 @@ Les paquets AUR ne devront jamais devenir une dépendance obligatoire du futur p
|
||||||
Depuis la racine du dépôt :
|
Depuis la racine du dépôt :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make
|
make -j8
|
||||||
```
|
```
|
||||||
|
|
||||||
Le binaire produit est :
|
Le binaire produit est :
|
||||||
|
|
@ -315,15 +346,15 @@ Le projet est compilé en C17 avec les avertissements traités comme des erreurs
|
||||||
Lancer tous les tests :
|
Lancer tous les tests :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make test
|
make -j8 test
|
||||||
```
|
```
|
||||||
|
|
||||||
Vérifications recommandées avant chaque commit :
|
Vérifications recommandées avant chaque commit :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make clean
|
make clean
|
||||||
make
|
make -j8
|
||||||
make test
|
make -j8 test
|
||||||
git diff --check
|
git diff --check
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -177,3 +177,64 @@ 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),
|
('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),
|
('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);
|
('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);
|
||||||
|
|
|
||||||
24
database/schema_v11.sql
Normal file
24
database/schema_v11.sql
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* 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);
|
||||||
55
database/schema_v12.sql
Normal file
55
database/schema_v12.sql
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* 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,''));
|
||||||
54
database/schema_v13.sql
Normal file
54
database/schema_v13.sql
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* 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;
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# Architecture
|
# Architecture
|
||||||
|
|
||||||
> **Version :** 3.0
|
> **Version :** 3.1
|
||||||
> **Dernière mise à jour :** 2026-07-24
|
> **Dernière mise à jour :** 2026-07-28
|
||||||
> **Schéma SQLite courant :** V10
|
> **Schéma SQLite courant :** V13
|
||||||
> **Statut :** architecture courante
|
> **Statut :** architecture courante
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -301,7 +301,7 @@ validation du chemin
|
||||||
↓
|
↓
|
||||||
création de l'arborescence
|
création de l'arborescence
|
||||||
↓
|
↓
|
||||||
initialisation transactionnelle de SQLite V10
|
initialisation transactionnelle de SQLite V13
|
||||||
↓
|
↓
|
||||||
création de l'identité de l'enquête
|
création de l'identité de l'enquête
|
||||||
↓
|
↓
|
||||||
|
|
@ -492,43 +492,168 @@ sont proches visuellement.
|
||||||
|
|
||||||
## 13. Pivot EML
|
## 13. Pivot EML
|
||||||
|
|
||||||
Statut :
|
### 13.1 Parcours
|
||||||
|
|
||||||
```text
|
```text
|
||||||
PARTIEL
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
La branche contient des briques d'analyse EML, d'extraction MIME, d'IBAN, OCR,
|
La preuve originale n'est jamais modifiée. Le staging et ses pièces jointes
|
||||||
ExifTool, vocabulaire contrôlé, propositions bancaires et pipeline de tâche.
|
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.
|
||||||
|
|
||||||
Le flux cible est :
|
### 13.2 Responsabilités
|
||||||
|
|
||||||
```text
|
- `EmlAnalyzer` lit les en-têtes, conserve leurs occurrences et qualifie les
|
||||||
EML original
|
adresses, domaines et IP.
|
||||||
↓
|
- `EmlMimeExtractor` parcourt les parties imbriquées, y compris
|
||||||
en-têtes et MIME
|
`message/rfc822`, décode Base64, quoted-printable, RFC 2047 et RFC 2231,
|
||||||
↓
|
assainit les noms et applique les limites de profondeur, nombre et taille.
|
||||||
pièces jointes dérivées
|
- `EmlPipelineTask` orchestre en arrière-plan l'analyse, le staging, les outils
|
||||||
↓
|
documentaires et les propositions bancaires.
|
||||||
empreintes et métadonnées
|
- `DocumentToolRunner` lance les programmes avec `GSubprocess`, sans shell,
|
||||||
↓
|
draine simultanément `stdout` et `stderr`, borne les sorties et propage
|
||||||
OCR et indicateurs
|
l'annulation.
|
||||||
↓
|
- les modules ExifTool, OCR et PDF structurent les résultats sans modifier la
|
||||||
valeurs brutes + normalisées + dérivées
|
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,
|
||||||
révision humaine
|
leur validation et une éventuelle correction OCR distincte.
|
||||||
↓
|
- `EmlAnalysisDialog` présente les résultats et collecte séparément la
|
||||||
intégration transactionnelle
|
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.
|
||||||
|
|
||||||
Les valeurs brutes restent immuables.
|
### 13.3 Proposition, observation et entité
|
||||||
|
|
||||||
Une IP de relais SMTP ne doit pas être présentée comme l'adresse personnelle
|
Une **proposition** est un résultat temporaire. Elle peut être rejetée,
|
||||||
d'un suspect.
|
invalidée, corrigée, conservée ou accompagnée d'une demande de promotion.
|
||||||
|
|
||||||
Une donnée bancaire observée ne prouve pas l'identité de l'auteur d'une fraude.
|
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.
|
||||||
|
|
||||||
Le ticket Forgejo #107 reste la référence fonctionnelle du chantier.
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Conventions de développement
|
# Conventions de développement
|
||||||
|
|
||||||
> **Version :** 2.0
|
> **Version :** 2.1
|
||||||
> **Dernière mise à jour :** 2026-07-24
|
> **Dernière mise à jour :** 2026-07-28
|
||||||
> **Projet :** Labfy Investigation
|
> **Projet :** Labfy Investigation
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -265,7 +265,7 @@ présentation graphique.
|
||||||
La version courante du schéma est :
|
La version courante du schéma est :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
V10
|
V13
|
||||||
```
|
```
|
||||||
|
|
||||||
Les scripts versionnés sont conservés dans :
|
Les scripts versionnés sont conservés dans :
|
||||||
|
|
@ -273,7 +273,7 @@ Les scripts versionnés sont conservés dans :
|
||||||
```text
|
```text
|
||||||
database/schema_v1.sql
|
database/schema_v1.sql
|
||||||
...
|
...
|
||||||
database/schema_v10.sql
|
database/schema_v12.sql
|
||||||
```
|
```
|
||||||
|
|
||||||
Le complément idempotent du schéma courant est :
|
Le complément idempotent du schéma courant est :
|
||||||
|
|
@ -614,3 +614,35 @@ HISTORIQUE
|
||||||
|
|
||||||
La priorité reste la qualité, la traçabilité et la compréhension durable du
|
La priorité reste la qualité, la traçabilité et la compréhension durable du
|
||||||
projet.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@
|
||||||
Ce fichier recense les outils utilisés par LabFy Investigation et leur
|
Ce fichier recense les outils utilisés par LabFy Investigation et leur
|
||||||
installation. Il sera complété à chaque intégration d’un nouvel outil.
|
installation. Il sera complété à chaque intégration d’un nouvel outil.
|
||||||
|
|
||||||
|
La migration SQLite V13 et la gestion des propriétaires de
|
||||||
|
`preuve_entites` n'ajoutent aucune dépendance système.
|
||||||
|
|
||||||
## Principe d’installation
|
## Principe d’installation
|
||||||
|
|
||||||
Les dépendances système sont installées avec le gestionnaire de paquets de la
|
Les dépendances système sont installées avec le gestionnaire de paquets de la
|
||||||
|
|
@ -27,8 +30,9 @@ export PATH="$HOME/.local/share/labfy-osint/bin:$PATH"
|
||||||
|
|
||||||
| Fonction | Exécutable | Arch Linux (pacman) | Ubuntu/Debian (apt) |
|
| Fonction | Exécutable | Arch Linux (pacman) | Ubuntu/Debian (apt) |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| OCR | `tesseract` | `sudo pacman -S tesseract tesseract-data-fra` | `sudo apt install tesseract-ocr tesseract-ocr-fra` |
|
| 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` |
|
||||||
| Métadonnées | `exiftool` | `sudo pacman -S perl-image-exiftool` | `sudo apt install libimage-exiftool-perl` |
|
| 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` |
|
| 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` |
|
| Résolution réseau | `host` | `sudo pacman -S bind` | `sudo apt install bind9-host` |
|
||||||
| WHOIS | `whois` | `sudo pacman -S whois` | `sudo apt install whois` |
|
| WHOIS | `whois` | `sudo pacman -S whois` | `sudo apt install whois` |
|
||||||
|
|
@ -40,7 +44,7 @@ export PATH="$HOME/.local/share/labfy-osint/bin:$PATH"
|
||||||
Après installation, vérifier les outils avec :
|
Après installation, vérifier les outils avec :
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
command -v tesseract exiftool dig host whois curl openssl qpdf john pdf2john
|
command -v tesseract exiftool pdfinfo pdftotext pdftoppm dig host whois curl openssl qpdf john pdf2john
|
||||||
```
|
```
|
||||||
|
|
||||||
## Outils OSINT Python
|
## Outils OSINT Python
|
||||||
|
|
@ -83,8 +87,9 @@ vérifiée avec `command -v`, puis avec son option de version (`--version` ou
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Ne pas installer ces outils dans le dépôt Git.
|
- 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
|
- Ne jamais stocker de jetons, mots de passe ou données privées dans ce
|
||||||
fichier.
|
fichier.
|
||||||
- Les versions peuvent varier selon la distribution ; noter ici toute
|
- Les versions peuvent varier selon la distribution ; noter ici toute
|
||||||
commande particulière nécessaire à Ubuntu des forces de l’ordre.
|
commande particulière nécessaire à Ubuntu des forces de l’ordre.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Guide de développement
|
# Guide de développement
|
||||||
|
|
||||||
> **Version :** 2.0
|
> **Version :** 2.1
|
||||||
> **Dernière mise à jour :** 2026-07-24
|
> **Dernière mise à jour :** 2026-07-28
|
||||||
> **Projet :** Labfy Investigation
|
> **Projet :** Labfy Investigation
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -86,6 +86,22 @@ 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
|
Les outils OSINT, OCR, métadonnées ou PDF sont documentés séparément et restent
|
||||||
optionnels sauf décision explicite.
|
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
|
## 4. Récupération du dépôt
|
||||||
|
|
@ -195,6 +211,42 @@ make tests/test_eml_pipeline_task
|
||||||
./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.
|
Le nom exact d'une cible doit être vérifié dans le Makefile.
|
||||||
|
|
||||||
### 6.3 Validation avant intégration
|
### 6.3 Validation avant intégration
|
||||||
|
|
@ -345,7 +397,7 @@ Lire :
|
||||||
docs/database/DATABASE_ARCHITECTURE.md
|
docs/database/DATABASE_ARCHITECTURE.md
|
||||||
docs/database/SCHEMA_AUDIT_CURRENT.md
|
docs/database/SCHEMA_AUDIT_CURRENT.md
|
||||||
database/schema_current.sql
|
database/schema_current.sql
|
||||||
database/schema_v10.sql
|
database/schema_v12.sql
|
||||||
src/database/database.c
|
src/database/database.c
|
||||||
src/database/schema.c
|
src/database/schema.c
|
||||||
tests/test_database.c
|
tests/test_database.c
|
||||||
|
|
@ -353,21 +405,24 @@ tests/test_database.c
|
||||||
|
|
||||||
### 9.2 Nouvelle version de schéma
|
### 9.2 Nouvelle version de schéma
|
||||||
|
|
||||||
Pour créer V11, par exemple :
|
Pour créer une nouvelle version après V13, par exemple V14 :
|
||||||
|
|
||||||
1. ajouter `database/schema_v11.sql` ;
|
1. ajouter `database/schema_v13.sql` ;
|
||||||
2. déclarer et implémenter `schema_install_v11()` ;
|
2. déclarer et implémenter `schema_install_v13()` ;
|
||||||
3. ajouter `database_migrate_v10_to_v11()` ;
|
3. ajouter `database_migrate_v12_to_v13()` ;
|
||||||
4. raccorder la migration dans la boucle vers la version courante ;
|
4. raccorder la migration dans la boucle vers la version courante ;
|
||||||
5. mettre à jour les constantes de version ;
|
5. mettre à jour les constantes de version ;
|
||||||
6. installer V11 lors de la création d'une base neuve ;
|
6. installer V13 lors de la création d'une base neuve ;
|
||||||
7. adapter `schema_current.sql` si nécessaire ;
|
7. adapter `schema_current.sql` si nécessaire ;
|
||||||
8. ajouter une fixture V10 vers V11 ;
|
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.
|
||||||
9. tester une base neuve ;
|
9. tester une base neuve ;
|
||||||
10. tester le rollback ;
|
10. tester le rollback ;
|
||||||
11. vérifier l'intégrité et les clés étrangères ;
|
11. vérifier l'intégrité et les clés étrangères ;
|
||||||
12. mettre à jour l'audit courant ;
|
12. mettre à jour l'audit courant ;
|
||||||
13. créer l'audit versionné de V11.
|
13. créer l'audit versionné de V13.
|
||||||
|
|
||||||
Ne pas réécrire une ancienne migration publiée pour changer sa signification.
|
Ne pas réécrire une ancienne migration publiée pour changer sa signification.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# Roadmap
|
# Roadmap
|
||||||
|
|
||||||
> **Dernière mise à jour :** 2026-07-24
|
> **Dernière mise à jour :** 2026-07-28
|
||||||
> **État du projet :** développement actif
|
> **État du projet :** développement actif
|
||||||
> **Schéma SQLite courant :** V10
|
> **Schéma SQLite courant :** V13
|
||||||
> **Usage opérationnel :** non prêt pour la production
|
> **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 ;
|
- création, validation et ouverture d'enquêtes ;
|
||||||
- session d'enquête remplaçable proprement ;
|
- session d'enquête remplaçable proprement ;
|
||||||
- infrastructure SQLite et migrations jusqu'à V10 ;
|
- infrastructure SQLite et migrations jusqu'à V13 ;
|
||||||
- couche Database, DAO et services métier ;
|
- couche Database, DAO et services métier ;
|
||||||
- import de preuves avec copie contrôlée et SHA-256 ;
|
- import de preuves avec copie contrôlée et SHA-256 ;
|
||||||
- vérification d'intégrité et reclassement des preuves ;
|
- vérification d'intégrité et reclassement des preuves ;
|
||||||
|
|
@ -135,7 +135,8 @@ La branche `main` contient notamment :
|
||||||
- types canoniques de relations ;
|
- types canoniques de relations ;
|
||||||
- vocabulaire contrôlé ;
|
- vocabulaire contrôlé ;
|
||||||
- table V10 `bank_account_entities` ;
|
- table V10 `bank_account_entities` ;
|
||||||
- premiers composants du pipeline EML.
|
- pivot EML raccordé à GTK, observations persistantes indépendantes des
|
||||||
|
entités, promotion facultative et retrait réversible.
|
||||||
|
|
||||||
Cette liste est une synthèse et non un contrat de stabilité.
|
Cette liste est une synthèse et non un contrat de stabilité.
|
||||||
|
|
||||||
|
|
@ -145,15 +146,13 @@ Cette liste est une synthèse et non un contrat de stabilité.
|
||||||
|
|
||||||
### Ticket #107 — Pivot e-mail forensique
|
### Ticket #107 — Pivot e-mail forensique
|
||||||
|
|
||||||
Statut :
|
Statut technique :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
PARTIEL
|
IMPLÉMENTÉ — VALIDATION DOCUMENTAIRE ET CORRECTION BLOQUANTE RESTANTES
|
||||||
```
|
```
|
||||||
|
|
||||||
Les briques préparatoires existent, mais le flux complet reste à terminer.
|
Le parcours livré est :
|
||||||
|
|
||||||
Objectif principal :
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
preuve EML
|
preuve EML
|
||||||
|
|
@ -171,30 +170,29 @@ OCR et détection d'indicateurs
|
||||||
normalisation sans perte de la valeur brute
|
normalisation sans perte de la valeur brute
|
||||||
↓
|
↓
|
||||||
interface de révision
|
interface de révision
|
||||||
↓
|
↓ conservation explicite
|
||||||
confirmation explicite
|
observation persistante dans la fiche
|
||||||
↓
|
↓ promotion facultative
|
||||||
intégration transactionnelle
|
entité créée ou réutilisée dans le graphe
|
||||||
↓
|
|
||||||
rafraîchissement des vues et du graphe
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Priorités immédiates :
|
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.
|
||||||
|
|
||||||
1. terminer l'extraction MIME robuste ;
|
V13 distingue désormais chaque propriétaire du rattachement
|
||||||
2. conserver les pièces jointes comme fichiers dérivés ;
|
`preuve_entites`. Un retrait EML enlève sa seule source
|
||||||
3. réunir EML, OCR, IBAN et ExifTool dans un pipeline unique ;
|
`eml_observation`; les sources manuelles, historiques et celles des autres
|
||||||
4. terminer l'interface de révision ;
|
observations restent protégées.
|
||||||
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é.
|
|
||||||
|
|
||||||
Le ticket reste ouvert tant que son flux complet et ses critères d'acceptation
|
Améliorations suivantes :
|
||||||
ne sont pas validés.
|
|
||||||
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
61
docs/testing/EML_PIVOT_MANUAL_TEST.md
Normal file
61
docs/testing/EML_PIVOT_MANUAL_TEST.md
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
# 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`.
|
||||||
|
|
@ -16,6 +16,7 @@ typedef struct BankProposal
|
||||||
char *id; /**< UUID de la proposition */
|
char *id; /**< UUID de la proposition */
|
||||||
char *raw_iban; /**< Graphie IBAN brute lue/OCR */
|
char *raw_iban; /**< Graphie IBAN brute lue/OCR */
|
||||||
char *normalized_iban; /**< IBAN nettoyé et majuscule */
|
char *normalized_iban; /**< IBAN nettoyé et majuscule */
|
||||||
|
char *raw_bic; /**< Graphie BIC brute observée */
|
||||||
char *bic; /**< BIC / SWIFT (8 ou 11 car) */
|
char *bic; /**< BIC / SWIFT (8 ou 11 car) */
|
||||||
char *holder_name; /**< Titulaire du compte */
|
char *holder_name; /**< Titulaire du compte */
|
||||||
char *bank_name; /**< Nom de la banque */
|
char *bank_name; /**< Nom de la banque */
|
||||||
|
|
@ -26,6 +27,7 @@ typedef struct BankProposal
|
||||||
char *account_number; /**< Numéro de compte (11 car) */
|
char *account_number; /**< Numéro de compte (11 car) */
|
||||||
char *rib_key; /**< Clé RIB (2 ch) */
|
char *rib_key; /**< Clé RIB (2 ch) */
|
||||||
gboolean is_iban_valid; /**< VRAI si MOD-97 et format valides */
|
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 */
|
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 *suggested_ocr_fix; /**< Proposition de correction OCR (ex: "O->0") */
|
||||||
char *verification_status; /**< Code contrôlé: proposed, confirmed, rejected, etc. */
|
char *verification_status; /**< Code contrôlé: proposed, confirmed, rejected, etc. */
|
||||||
|
|
|
||||||
73
include/core/document_analysis.h
Normal file
73
include/core/document_analysis.h
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* @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
|
||||||
48
include/core/document_file_analysis.h
Normal file
48
include/core/document_file_analysis.h
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* @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
|
||||||
46
include/core/document_tool_runner.h
Normal file
46
include/core/document_tool_runner.h
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* @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
|
||||||
|
|
@ -8,6 +8,15 @@
|
||||||
G_BEGIN_DECLS
|
G_BEGIN_DECLS
|
||||||
/** @brief Résultat opaque d'une analyse d'en-têtes EML. */
|
/** @brief Résultat opaque d'une analyse d'en-têtes EML. */
|
||||||
typedef struct EmlAnalysis EmlAnalysis;
|
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.
|
* @brief Analyse uniquement les en-têtes d'un fichier EML local.
|
||||||
* @param file_path Chemin du fichier original en lecture seule.
|
* @param file_path Chemin du fichier original en lecture seule.
|
||||||
|
|
@ -27,6 +36,10 @@ const GPtrArray *eml_analysis_get_header_values(const EmlAnalysis *analysis,
|
||||||
const GPtrArray *eml_analysis_get_email_addresses(const EmlAnalysis *analysis);
|
const GPtrArray *eml_analysis_get_email_addresses(const EmlAnalysis *analysis);
|
||||||
/** @brief Retourne les domaines uniques extraits. */
|
/** @brief Retourne les domaines uniques extraits. */
|
||||||
const GPtrArray *eml_analysis_get_domains(const EmlAnalysis *analysis);
|
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. */
|
/** @brief Retourne les adresses IP uniques extraites. */
|
||||||
const GPtrArray *eml_analysis_get_ip_addresses(const EmlAnalysis *analysis);
|
const GPtrArray *eml_analysis_get_ip_addresses(const EmlAnalysis *analysis);
|
||||||
/** @brief Retourne les IP présentes dans la partie `from` des Received. */
|
/** @brief Retourne les IP présentes dans la partie `from` des Received. */
|
||||||
|
|
@ -37,5 +50,11 @@ const GPtrArray *eml_analysis_get_destination_ip_addresses(
|
||||||
const EmlAnalysis *analysis);
|
const EmlAnalysis *analysis);
|
||||||
/** @brief Retourne une copie UTF-8 des en-têtes bruts. */
|
/** @brief Retourne une copie UTF-8 des en-têtes bruts. */
|
||||||
const char *eml_analysis_get_raw_headers(const EmlAnalysis *analysis);
|
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
|
G_END_DECLS
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,11 @@ typedef struct {
|
||||||
char *value;
|
char *value;
|
||||||
char *verification_status;
|
char *verification_status;
|
||||||
char *provenance_kind;
|
char *provenance_kind;
|
||||||
|
char *value_raw;
|
||||||
|
char *role;
|
||||||
|
char *source_header;
|
||||||
|
guint occurrence;
|
||||||
|
gboolean promote_to_entity;
|
||||||
} EmlEntityProposal;
|
} EmlEntityProposal;
|
||||||
/** @brief Crée une proposition possédée. */
|
/** @brief Crée une proposition possédée. */
|
||||||
EmlEntityProposal *eml_entity_proposal_new(const char *type_identifier,
|
EmlEntityProposal *eml_entity_proposal_new(const char *type_identifier,
|
||||||
|
|
@ -20,6 +25,11 @@ EmlEntityProposal *eml_entity_proposal_new(const char *type_identifier,
|
||||||
EmlEntityProposal *eml_entity_proposal_new_with_metadata(
|
EmlEntityProposal *eml_entity_proposal_new_with_metadata(
|
||||||
const char *type_identifier, const char *value,
|
const char *type_identifier, const char *value,
|
||||||
const char *verification_status, const char *provenance_kind);
|
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. */
|
/** @brief Libère une proposition. */
|
||||||
void eml_entity_proposal_free(EmlEntityProposal *proposal);
|
void eml_entity_proposal_free(EmlEntityProposal *proposal);
|
||||||
/**
|
/**
|
||||||
|
|
@ -34,6 +44,12 @@ void eml_entity_proposal_free(EmlEntityProposal *proposal);
|
||||||
*/
|
*/
|
||||||
gboolean eml_integration_apply(Database *database,
|
gboolean eml_integration_apply(Database *database,
|
||||||
const char *evidence_identifier, const GPtrArray *proposals,
|
const char *evidence_identifier, const GPtrArray *proposals,
|
||||||
guint *out_created, guint *out_reused, GError **error);
|
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);
|
||||||
G_END_DECLS
|
G_END_DECLS
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -5,23 +5,39 @@
|
||||||
#ifndef LABFY_INVESTIGATION_EML_MIME_EXTRACTOR_H
|
#ifndef LABFY_INVESTIGATION_EML_MIME_EXTRACTOR_H
|
||||||
#define LABFY_INVESTIGATION_EML_MIME_EXTRACTOR_H
|
#define LABFY_INVESTIGATION_EML_MIME_EXTRACTOR_H
|
||||||
|
|
||||||
#include <glib.h>
|
#include <gio/gio.h>
|
||||||
|
|
||||||
G_BEGIN_DECLS
|
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. */
|
/** @brief Représentation d'une pièce jointe extraite d'un message EML. */
|
||||||
typedef struct EmlAttachment
|
typedef struct EmlAttachment
|
||||||
{
|
{
|
||||||
char *part_index; /**< Chemin/index MIME (ex: "1.2") */
|
char *part_index; /**< Chemin/index MIME (ex: "1.2") */
|
||||||
char *declared_filename; /**< Nom de fichier d'origine */
|
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 *sanitized_filename; /**< Nom assaini (anti path-traversal) */
|
||||||
char *extracted_path; /**< Chemin absolu dans 02_Preuves_Traitees */
|
char *extracted_path; /**< Chemin absolu dans 02_Preuves_Traitees */
|
||||||
char *relative_path; /**< Chemin relatif par rapport à la racine d'enquête */
|
char *relative_path; /**< Chemin relatif par rapport à la racine d'enquête */
|
||||||
char *content_type; /**< Type MIME déclaré */
|
char *content_type; /**< Type MIME déclaré */
|
||||||
char *detected_mime; /**< Type MIME détecté */
|
char *detected_mime; /**< Type MIME détecté */
|
||||||
char *content_id; /**< Content-ID pour les images/pièces inline */
|
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 *transfer_encoding; /**< Content-Transfer-Encoding */
|
||||||
|
char *extracted_at_utc; /**< Date UTC de l'extraction */
|
||||||
gboolean is_inline; /**< VRAI si disposition inline */
|
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 encoded_size; /**< Taille encodée */
|
||||||
gsize decoded_size; /**< Taille décodée */
|
gsize decoded_size; /**< Taille décodée */
|
||||||
char *sha256; /**< Empreinte SHA-256 du fichier extrait */
|
char *sha256; /**< Empreinte SHA-256 du fichier extrait */
|
||||||
|
|
@ -63,6 +79,22 @@ EmlMimeResult *eml_mime_extract_attachments(const char *eml_path,
|
||||||
const char *target_dir,
|
const char *target_dir,
|
||||||
GError **error);
|
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
|
G_END_DECLS
|
||||||
|
|
||||||
#endif /* LABFY_INVESTIGATION_EML_MIME_EXTRACTOR_H */
|
#endif /* LABFY_INVESTIGATION_EML_MIME_EXTRACTOR_H */
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
#include "core/bank_proposal.h"
|
#include "core/bank_proposal.h"
|
||||||
#include "core/eml_analyzer.h"
|
#include "core/eml_analyzer.h"
|
||||||
#include "core/eml_mime_extractor.h"
|
#include "core/eml_mime_extractor.h"
|
||||||
|
#include "core/document_file_analysis.h"
|
||||||
|
|
||||||
G_BEGIN_DECLS
|
G_BEGIN_DECLS
|
||||||
|
|
||||||
|
|
@ -18,7 +19,10 @@ typedef struct EmlPipelineResult
|
||||||
EmlAnalysis *analysis; /**< Analyse des en-têtes EML */
|
EmlAnalysis *analysis; /**< Analyse des en-têtes EML */
|
||||||
EmlMimeResult *mime_result; /**< Pièces jointes extraites */
|
EmlMimeResult *mime_result; /**< Pièces jointes extraites */
|
||||||
GPtrArray *bank_proposals; /**< Tableau de BankProposal* */
|
GPtrArray *bank_proposals; /**< Tableau de BankProposal* */
|
||||||
|
GPtrArray *document_analyses; /**< Tableau de DocumentFileAnalysis* */
|
||||||
GPtrArray *warnings; /**< Avertissements globaux */
|
GPtrArray *warnings; /**< Avertissements globaux */
|
||||||
|
DocumentAnalysisState state;
|
||||||
|
guint skipped_document_analyses;
|
||||||
} EmlPipelineResult;
|
} EmlPipelineResult;
|
||||||
|
|
||||||
/** @brief Libère un résultat EmlPipelineResult. */
|
/** @brief Libère un résultat EmlPipelineResult. */
|
||||||
|
|
@ -35,6 +39,31 @@ BackgroundTask *eml_pipeline_task_new(const char *eml_path,
|
||||||
const char *processed_evidence_dir,
|
const char *processed_evidence_dir,
|
||||||
const char *evidence_id);
|
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
|
G_END_DECLS
|
||||||
|
|
||||||
#endif /* LABFY_INVESTIGATION_EML_PIPELINE_TASK_H */
|
#endif /* LABFY_INVESTIGATION_EML_PIPELINE_TASK_H */
|
||||||
|
|
|
||||||
42
include/core/exiftool_analysis.h
Normal file
42
include/core/exiftool_analysis.h
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* @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
|
||||||
39
include/core/ocr_analysis.h
Normal file
39
include/core/ocr_analysis.h
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* @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
|
||||||
63
include/core/pdf_analysis.h
Normal file
63
include/core/pdf_analysis.h
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* @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
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
#define LABFY_INVESTIGATION_EVIDENCE_ENTITY_DAO_H
|
#define LABFY_INVESTIGATION_EVIDENCE_ENTITY_DAO_H
|
||||||
|
|
||||||
#include "database/database.h"
|
#include "database/database.h"
|
||||||
|
#include "models/evidence_observation.h"
|
||||||
|
|
||||||
#include <glib.h>
|
#include <glib.h>
|
||||||
|
|
||||||
|
|
@ -150,6 +151,16 @@ gboolean evidence_entity_dao_unlink(
|
||||||
GError **error
|
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.
|
* @brief Vérifie si une association existe.
|
||||||
*
|
*
|
||||||
|
|
@ -213,6 +224,27 @@ GPtrArray *evidence_entity_dao_list_evidence_identifiers(
|
||||||
GError **error
|
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
|
G_END_DECLS
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,9 @@ bool schema_install_v7(Database *database);
|
||||||
bool schema_install_v8(Database *database);
|
bool schema_install_v8(Database *database);
|
||||||
bool schema_install_v9(Database *database);
|
bool schema_install_v9(Database *database);
|
||||||
bool schema_install_v10(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.
|
* @brief Garantit la présence des extensions du schéma courant V2.
|
||||||
|
|
|
||||||
18
include/models/evidence_observation.h
Normal file
18
include/models/evidence_observation.h
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#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
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
#define LABFY_INVESTIGATION_EML_ANALYSIS_DIALOG_H
|
#define LABFY_INVESTIGATION_EML_ANALYSIS_DIALOG_H
|
||||||
#include "core/eml_processing.h"
|
#include "core/eml_processing.h"
|
||||||
#include "core/eml_integration.h"
|
#include "core/eml_integration.h"
|
||||||
|
#include "core/eml_pipeline_task.h"
|
||||||
#include <gtk/gtk.h>
|
#include <gtk/gtk.h>
|
||||||
G_BEGIN_DECLS
|
G_BEGIN_DECLS
|
||||||
/** @brief Callback recevant les propositions sélectionnées, ou NULL. */
|
/** @brief Callback recevant les propositions sélectionnées, ou NULL. */
|
||||||
|
|
@ -19,5 +20,14 @@ typedef void (*EmlAnalysisDialogCallback)(GPtrArray *proposals,
|
||||||
void eml_analysis_dialog_present(GtkWindow *parent,
|
void eml_analysis_dialog_present(GtkWindow *parent,
|
||||||
const EmlProcessingResult *result, EmlAnalysisDialogCallback callback,
|
const EmlProcessingResult *result, EmlAnalysisDialogCallback callback,
|
||||||
gpointer user_data);
|
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
|
G_END_DECLS
|
||||||
#endif
|
#endif
|
||||||
|
|
|
||||||
|
|
@ -497,6 +497,14 @@ void main_window_set_selected_evidence(
|
||||||
MainWindow *main_window,
|
MainWindow *main_window,
|
||||||
const EvidenceRecord *evidence_record
|
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. */
|
/** @brief Transmet au workspace un chemin absolu validé pour l'aperçu. */
|
||||||
void main_window_set_evidence_preview(MainWindow *main_window,
|
void main_window_set_evidence_preview(MainWindow *main_window,
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
#include "core/investigation_node.h"
|
#include "core/investigation_node.h"
|
||||||
#include "models/evidence_record.h"
|
#include "models/evidence_record.h"
|
||||||
|
#include "models/evidence_observation.h"
|
||||||
#include "models/entity_record.h"
|
#include "models/entity_record.h"
|
||||||
#include "models/osint_action_catalog.h"
|
#include "models/osint_action_catalog.h"
|
||||||
|
|
||||||
|
|
@ -153,6 +154,8 @@ typedef void (*WorkspaceExtractMetadataCallback)(const char *evidence_identifier
|
||||||
/** @brief Callback appelé pour récupérer le mot de passe d'un PDF. */
|
/** @brief Callback appelé pour récupérer le mot de passe d'un PDF. */
|
||||||
typedef void (*WorkspaceRecoverPdfPasswordCallback)(
|
typedef void (*WorkspaceRecoverPdfPasswordCallback)(
|
||||||
const char *evidence_identifier, gpointer user_data);
|
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.
|
* @brief Crée une nouvelle zone de travail.
|
||||||
|
|
@ -203,6 +206,14 @@ void workspace_set_selected_evidence(
|
||||||
Workspace *workspace,
|
Workspace *workspace,
|
||||||
const EvidenceRecord *evidence_record
|
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.
|
* @brief Affiche l'aperçu local d'un fichier image ou vidéo.
|
||||||
|
|
|
||||||
151
opencode.json.backup-7b
Normal file
151
opencode.json.backup-7b
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
{
|
||||||
|
"$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 l’architecte, 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 l’implémentation. Transmets ensuite à coder-c17 tout le contexte nécessaire : objectif, fichiers pertinents, interfaces, extraits de code, contraintes et diagnostics. Le Coder ne dispose d’aucun outil : il renvoie uniquement du code ou un patch textuel. Tu restes seul responsable de l’application des changements, de l’exé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 l’orchestrateur 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 d’aucun 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, l’ownership GLib, l’architecture 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 l’architecture, la cohérence des API, l’ownership, 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 l’architecte 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, l’ownership et les durées de vie GLib/GTK, les transactions SQLite, la gestion d’erreurs, 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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -58,6 +58,8 @@
|
||||||
#include "core/person_entity_service.h"
|
#include "core/person_entity_service.h"
|
||||||
#include "core/eml_processing.h"
|
#include "core/eml_processing.h"
|
||||||
#include "core/eml_integration.h"
|
#include "core/eml_integration.h"
|
||||||
|
#include "core/eml_pipeline_task.h"
|
||||||
|
#include "core/file_hash.h"
|
||||||
#include "database/database.h"
|
#include "database/database.h"
|
||||||
#include "database/transaction.h"
|
#include "database/transaction.h"
|
||||||
#include "views/create_social_account_dialog.h"
|
#include "views/create_social_account_dialog.h"
|
||||||
|
|
@ -148,6 +150,22 @@ struct Application
|
||||||
char *pending_entity_selection_identifier;
|
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 d’un import.
|
* @brief Contexte ApplicationOpenErrorconservé jusqu’à la fin d’un import.
|
||||||
*/
|
*/
|
||||||
|
|
@ -249,7 +267,15 @@ typedef struct
|
||||||
static char *application_osint_output_to_utf8(GBytes *bytes);
|
static char *application_osint_output_to_utf8(GBytes *bytes);
|
||||||
static char *application_osint_create_timestamp(void);
|
static char *application_osint_create_timestamp(void);
|
||||||
/** @brief Contexte possédé pendant la révision d'une analyse EML. */
|
/** @brief Contexte possédé pendant la révision d'une analyse EML. */
|
||||||
typedef struct { Application *application; char *evidence_identifier; }
|
typedef struct {
|
||||||
|
Application *application;
|
||||||
|
InvestigationSession *expected_session;
|
||||||
|
char *evidence_identifier;
|
||||||
|
char *evidence_name;
|
||||||
|
char *relative_path;
|
||||||
|
char *source_sha256;
|
||||||
|
char *staging_directory;
|
||||||
|
}
|
||||||
ApplicationEmlReviewContext;
|
ApplicationEmlReviewContext;
|
||||||
|
|
||||||
static void application_start_graph_loading(
|
static void application_start_graph_loading(
|
||||||
|
|
@ -3738,7 +3764,48 @@ cleanup:
|
||||||
|
|
||||||
/** @brief Libère le contexte de révision EML. */
|
/** @brief Libère le contexte de révision EML. */
|
||||||
static void application_eml_review_context_free(ApplicationEmlReviewContext *context)
|
static void application_eml_review_context_free(ApplicationEmlReviewContext *context)
|
||||||
{ if (context == NULL) return; g_free(context->evidence_identifier); g_free(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);
|
||||||
|
}
|
||||||
|
|
||||||
/** @brief Intègre la sélection EML puis actualise le graphe. */
|
/** @brief Intègre la sélection EML puis actualise le graphe. */
|
||||||
static void application_on_eml_selection_completed(
|
static void application_on_eml_selection_completed(
|
||||||
|
|
@ -3747,29 +3814,96 @@ static void application_on_eml_selection_completed(
|
||||||
ApplicationEmlReviewContext *context = user_data;
|
ApplicationEmlReviewContext *context = user_data;
|
||||||
Application *application = context != NULL ? context->application : NULL;
|
Application *application = context != NULL ? context->application : NULL;
|
||||||
const InvestigationProject *project = NULL;
|
const InvestigationProject *project = NULL;
|
||||||
GError *error = NULL; guint created = 0, reused = 0; char *message = NULL;
|
GError *error = NULL; guint observations = 0, created = 0, reused = 0;
|
||||||
|
char *message = NULL;
|
||||||
if (proposals == NULL)
|
if (proposals == NULL)
|
||||||
{ application_eml_review_context_free(context); return; }
|
{
|
||||||
|
application_remove_staging_directory(context != NULL
|
||||||
|
? context->staging_directory : NULL);
|
||||||
|
application_eml_review_context_free(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (application == NULL || application->session == NULL ||
|
if (application == NULL || application->session == NULL ||
|
||||||
!eml_integration_apply(investigation_session_get_database(application->session),
|
!eml_integration_apply(investigation_session_get_database(application->session),
|
||||||
context->evidence_identifier, proposals, &created, &reused, &error))
|
context->evidence_identifier, proposals, &observations,
|
||||||
|
&created, &reused, &error))
|
||||||
application_present_error(application, "Intégration EML impossible",
|
application_present_error(application, "Intégration EML impossible",
|
||||||
error != NULL ? error->message : "La transaction a échoué.");
|
error != NULL ? error->message : "La transaction a échoué.");
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
message = g_strdup_printf("%u entité(s) créée(s), %u réutilisée(s).",
|
message = g_strdup_printf(
|
||||||
created, reused);
|
"%u observation(s) conservée(s), %u entité(s) créée(s), "
|
||||||
|
"%u réutilisée(s).", observations, created, reused);
|
||||||
application_message_dialog_present(main_window_get_window(application->main_window),
|
application_message_dialog_present(main_window_get_window(application->main_window),
|
||||||
APPLICATION_MESSAGE_DIALOG_INFORMATION, "Analyse EML intégrée", message);
|
APPLICATION_MESSAGE_DIALOG_INFORMATION, "Analyse EML intégrée", message);
|
||||||
|
if (created + reused > 0)
|
||||||
|
{
|
||||||
project = investigation_session_get_project(application->session);
|
project = investigation_session_get_project(application->session);
|
||||||
application_start_graph_loading(application,
|
application_start_graph_loading(application,
|
||||||
investigation_project_get_database_path(project));
|
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);
|
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);
|
application_eml_review_context_free(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @brief Prépare une copie vérifiée puis affiche l'analyse EML locale. */
|
/** @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 l’e-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 n’a 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. */
|
||||||
static void application_on_analyze_eml_requested(
|
static void application_on_analyze_eml_requested(
|
||||||
const char *evidence_identifier, gpointer user_data)
|
const char *evidence_identifier, gpointer user_data)
|
||||||
{
|
{
|
||||||
|
|
@ -3777,8 +3911,14 @@ static void application_on_analyze_eml_requested(
|
||||||
const InvestigationProject *project = NULL;
|
const InvestigationProject *project = NULL;
|
||||||
EvidenceDao *dao = NULL;
|
EvidenceDao *dao = NULL;
|
||||||
EvidenceRecord *record = NULL;
|
EvidenceRecord *record = NULL;
|
||||||
EmlProcessingResult *result = NULL;
|
|
||||||
ApplicationEmlReviewContext *context = 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;
|
GError *error = NULL;
|
||||||
if (application == NULL || application->session == NULL ||
|
if (application == NULL || application->session == NULL ||
|
||||||
evidence_identifier == NULL) return;
|
evidence_identifier == NULL) return;
|
||||||
|
|
@ -3787,22 +3927,93 @@ static void application_on_analyze_eml_requested(
|
||||||
if (dao != NULL) record = evidence_dao_find_by_identifier(
|
if (dao != NULL) record = evidence_dao_find_by_identifier(
|
||||||
dao, evidence_identifier, &error);
|
dao, evidence_identifier, &error);
|
||||||
project = investigation_session_get_project(application->session);
|
project = investigation_session_get_project(application->session);
|
||||||
if (record != NULL && project != NULL)
|
if (record == NULL || project == NULL) goto failure;
|
||||||
result = eml_processing_prepare(
|
root = g_canonicalize_filename(
|
||||||
investigation_project_get_root_path(project), record, &error);
|
investigation_project_get_root_path(project), NULL);
|
||||||
if (result == NULL)
|
candidate = g_build_filename(root,
|
||||||
application_present_error(application, "Analyse EML impossible",
|
evidence_record_get_relative_path(record), NULL);
|
||||||
error != NULL ? error->message : "La copie de travail n'a pas pu être analysée.");
|
source_path = g_canonicalize_filename(candidate, NULL);
|
||||||
else
|
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))
|
||||||
{
|
{
|
||||||
|
g_set_error_literal(&error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
|
||||||
|
"La preuve sélectionnée n’est pas un fichier EML exploitable.");
|
||||||
|
goto failure;
|
||||||
|
}
|
||||||
|
if (!file_hash_compute_sha256(source_path, NULL, ¤t_sha256,
|
||||||
|
¤t_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,
|
||||||
|
"L’empreinte SHA-256 de la preuve diverge de "
|
||||||
|
"l’empreinte enregistrée. L’analyse 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 = g_new0(ApplicationEmlReviewContext, 1);
|
||||||
context->application = application;
|
context->application = application;
|
||||||
|
context->expected_session = application->session;
|
||||||
context->evidence_identifier = g_strdup(evidence_identifier);
|
context->evidence_identifier = g_strdup(evidence_identifier);
|
||||||
eml_analysis_dialog_present(main_window_get_window(application->main_window),
|
context->evidence_name = g_strdup(name);
|
||||||
result, application_on_eml_selection_completed, context);
|
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_processing_result_free(result); evidence_record_free(record);
|
main_window_set_status(application->main_window,
|
||||||
evidence_dao_free(dao); g_clear_error(&error);
|
"Analyse de l’e-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 n’a 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @brief Contexte possédé pendant la révision OCR d'un RIB. */
|
/** @brief Contexte possédé pendant la révision OCR d'un RIB. */
|
||||||
|
|
@ -3858,14 +4069,8 @@ static void application_on_rib_ocr_confirmed(const char *iban,
|
||||||
}
|
}
|
||||||
link_dao = evidence_entity_dao_new(database, &error);
|
link_dao = evidence_entity_dao_new(database, &error);
|
||||||
if (link_dao == NULL) goto failure;
|
if (link_dao == NULL) goto failure;
|
||||||
{
|
if (!evidence_entity_dao_link(link_dao, context->evidence_identifier,
|
||||||
gboolean exists = FALSE;
|
entity_identifier, &error)) goto failure;
|
||||||
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;
|
if (!database_transaction_commit(database)) goto failure;
|
||||||
active = FALSE; project = investigation_session_get_project(application->session);
|
active = FALSE; project = investigation_session_get_project(application->session);
|
||||||
g_free(application->pending_entity_selection_identifier);
|
g_free(application->pending_entity_selection_identifier);
|
||||||
|
|
@ -5490,11 +5695,27 @@ static void application_on_evidence_selected(
|
||||||
application->main_window,
|
application->main_window,
|
||||||
evidence_record
|
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);
|
project = investigation_session_get_project(application->session);
|
||||||
investigation_root = project != NULL
|
investigation_root = project != NULL
|
||||||
? investigation_project_get_root_path(project) : NULL;
|
? investigation_project_get_root_path(project) : NULL;
|
||||||
relative_path = evidence_record_get_relative_path(evidence_record);
|
relative_path = evidence_record_get_relative_path(evidence_record);
|
||||||
|
gboolean eml_analysis_available = FALSE;
|
||||||
if (investigation_root != NULL && relative_path != NULL)
|
if (investigation_root != NULL && relative_path != NULL)
|
||||||
{
|
{
|
||||||
canonical_root = g_canonicalize_filename(investigation_root, NULL);
|
canonical_root = g_canonicalize_filename(investigation_root, NULL);
|
||||||
|
|
@ -5506,11 +5727,20 @@ static void application_on_evidence_selected(
|
||||||
canonical_path[strlen(canonical_root)] == G_DIR_SEPARATOR &&
|
canonical_path[strlen(canonical_root)] == G_DIR_SEPARATOR &&
|
||||||
g_file_test(canonical_path, G_FILE_TEST_IS_REGULAR))
|
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,
|
main_window_set_evidence_preview(application->main_window,
|
||||||
canonical_path,
|
canonical_path,
|
||||||
evidence_record_get_original_name(evidence_record));
|
evidence_record_get_original_name(evidence_record));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
main_window_set_eml_analysis_available(
|
||||||
|
application->main_window, eml_analysis_available);
|
||||||
|
|
||||||
original_name =
|
original_name =
|
||||||
evidence_record_get_original_name(
|
evidence_record_get_original_name(
|
||||||
|
|
@ -6180,11 +6410,8 @@ static void application_on_person_evidences_selected(GPtrArray *selected,
|
||||||
for (guint index = 0; index < selected->len; index++)
|
for (guint index = 0; index < selected->len; index++)
|
||||||
{
|
{
|
||||||
const char *identifier = g_ptr_array_index(selected, index);
|
const char *identifier = g_ptr_array_index(selected, index);
|
||||||
gboolean exists = FALSE;
|
if (!evidence_entity_dao_link(dao, identifier,
|
||||||
if (!evidence_entity_dao_exists(dao, identifier,
|
context->entity_identifier, &error)) goto failure;
|
||||||
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;
|
if (!database_transaction_commit(database)) goto failure;
|
||||||
active = FALSE;
|
active = FALSE;
|
||||||
|
|
@ -7822,6 +8049,60 @@ 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",
|
||||||
|
"L’observation restera dans la fiche. Le nœud sera supprimé "
|
||||||
|
"uniquement s’il n’est utilisé par aucune autre preuve, observation "
|
||||||
|
"ou relation.", "Retirer du graphe",
|
||||||
|
application_on_observation_remove_confirmed, context);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Ferme proprement l'application.
|
* @brief Ferme proprement l'application.
|
||||||
*
|
*
|
||||||
|
|
@ -7905,6 +8186,8 @@ static void application_on_activate(
|
||||||
application_on_evidence_selected,
|
application_on_evidence_selected,
|
||||||
application
|
application
|
||||||
);
|
);
|
||||||
|
main_window_set_observation_remove_callback(application->main_window,
|
||||||
|
application_on_observation_remove_requested, application);
|
||||||
|
|
||||||
main_window_set_verify_evidence_callback(
|
main_window_set_verify_evidence_callback(
|
||||||
application->main_window,
|
application->main_window,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
* @brief Détection, normalisation et modèle de proposition bancaire (IBAN, RIB, BIC).
|
* @brief Détection, normalisation et modèle de proposition bancaire (IBAN, RIB, BIC).
|
||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
#include "core/bank_proposal.h"
|
#include "core/bank_proposal.h"
|
||||||
|
#include "core/iban_analyzer.h"
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
|
@ -15,6 +16,7 @@ void bank_proposal_free(BankProposal *p)
|
||||||
g_free(p->id);
|
g_free(p->id);
|
||||||
g_free(p->raw_iban);
|
g_free(p->raw_iban);
|
||||||
g_free(p->normalized_iban);
|
g_free(p->normalized_iban);
|
||||||
|
g_free(p->raw_bic);
|
||||||
g_free(p->bic);
|
g_free(p->bic);
|
||||||
g_free(p->holder_name);
|
g_free(p->holder_name);
|
||||||
g_free(p->bank_name);
|
g_free(p->bank_name);
|
||||||
|
|
@ -24,6 +26,7 @@ void bank_proposal_free(BankProposal *p)
|
||||||
g_free(p->branch_code);
|
g_free(p->branch_code);
|
||||||
g_free(p->account_number);
|
g_free(p->account_number);
|
||||||
g_free(p->rib_key);
|
g_free(p->rib_key);
|
||||||
|
g_free(p->iban_validation);
|
||||||
g_free(p->suggested_ocr_fix);
|
g_free(p->suggested_ocr_fix);
|
||||||
g_free(p->verification_status);
|
g_free(p->verification_status);
|
||||||
g_free(p->provenance_kind);
|
g_free(p->provenance_kind);
|
||||||
|
|
@ -34,22 +37,143 @@ void bank_proposal_free(BankProposal *p)
|
||||||
g_free(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)
|
gboolean bank_proposal_validate_iban(const char *iban)
|
||||||
{
|
{
|
||||||
if (iban == NULL)
|
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)
|
||||||
return FALSE;
|
return FALSE;
|
||||||
|
|
||||||
gsize len = strlen(iban);
|
gsize len = strlen(normalized);
|
||||||
if (len < 15 || len > 34)
|
if (len < 15 || len > 34)
|
||||||
|
{
|
||||||
|
g_free(normalized);
|
||||||
return FALSE;
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
/* Vérification des 2 premières lettres (Code pays) */
|
/* Vérification des 2 premières lettres (Code pays) */
|
||||||
if (!g_ascii_isalpha(iban[0]) || !g_ascii_isalpha(iban[1]))
|
if (!g_ascii_isalpha(normalized[0]) ||
|
||||||
|
!g_ascii_isalpha(normalized[1]) ||
|
||||||
|
!g_ascii_isdigit(normalized[2]) ||
|
||||||
|
!g_ascii_isdigit(normalized[3]))
|
||||||
|
{
|
||||||
|
g_free(normalized);
|
||||||
return FALSE;
|
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 */
|
/* Repositionnement des 4 premiers caractères à la fin */
|
||||||
GString *rearranged = g_string_new(iban + 4);
|
GString *rearranged = g_string_new(normalized + 4);
|
||||||
g_string_append_len(rearranged, iban, 4);
|
g_string_append_len(rearranged, normalized, 4);
|
||||||
|
g_free(normalized);
|
||||||
|
|
||||||
/* Conversion des lettres en chiffres (A=10, Z=35) */
|
/* Conversion des lettres en chiffres (A=10, Z=35) */
|
||||||
GString *numeric = g_string_new(NULL);
|
GString *numeric = g_string_new(NULL);
|
||||||
|
|
@ -99,12 +223,7 @@ gboolean bank_proposal_validate_bic(const char *bic)
|
||||||
if (len != 8 && len != 11)
|
if (len != 8 && len != 11)
|
||||||
return FALSE;
|
return FALSE;
|
||||||
|
|
||||||
for (gsize i = 0; i < 4; i++)
|
for (gsize i = 0; i < 6; i++)
|
||||||
{
|
|
||||||
if (!g_ascii_isalpha(bic[i]))
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
for (gsize i = 4; i < 6; i++)
|
|
||||||
{
|
{
|
||||||
if (!g_ascii_isalpha(bic[i]))
|
if (!g_ascii_isalpha(bic[i]))
|
||||||
return FALSE;
|
return FALSE;
|
||||||
|
|
@ -144,43 +263,42 @@ gboolean bank_proposal_derive_french_rib(BankProposal *proposal)
|
||||||
|
|
||||||
BankProposal *bank_proposal_analyze_text(const char *raw_text, const char *evidence_id)
|
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')
|
if (raw_text == NULL || raw_text[0] == '\0')
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
/* Nettoyage des espaces pour recherche d'IBAN */
|
iban_regex = g_regex_new(
|
||||||
GString *clean = g_string_new(NULL);
|
"(?i)\\b[A-Z]{2}[0-9]{2}(?:[ \\t-]*[A-Z0-9]){11,30}\\b",
|
||||||
gsize raw_len = strlen(raw_text);
|
G_REGEX_OPTIMIZE,
|
||||||
for (gsize i = 0; i < raw_len; i++)
|
0,
|
||||||
{
|
NULL
|
||||||
char c = raw_text[i];
|
);
|
||||||
if (g_ascii_isalnum(c))
|
g_regex_match(iban_regex, raw_text, 0, &iban_match);
|
||||||
{
|
if (g_match_info_matches(iban_match))
|
||||||
g_string_append_c(clean, g_ascii_toupper(c));
|
raw_iban = g_match_info_fetch(iban_match, 0);
|
||||||
}
|
g_match_info_free(iban_match);
|
||||||
}
|
g_regex_unref(iban_regex);
|
||||||
|
|
||||||
/* Recherche de motif IBAN (ex: FR76...) */
|
normalized_iban = iban_analyzer_normalize(raw_iban);
|
||||||
const char *data = clean->str;
|
if (normalized_iban == NULL)
|
||||||
const char *iban_start = strstr(data, "FR");
|
|
||||||
if (iban_start == NULL)
|
|
||||||
{
|
{
|
||||||
/* Essai avec d'autres codes pays à 2 lettres */
|
g_free(raw_iban);
|
||||||
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;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
BankProposal *p = g_new0(BankProposal, 1);
|
BankProposal *p = g_new0(BankProposal, 1);
|
||||||
p->id = g_uuid_string_random();
|
p->id = g_uuid_string_random();
|
||||||
p->raw_iban = g_strdup(raw_text);
|
p->raw_iban = raw_iban;
|
||||||
p->normalized_iban = g_strndup(iban_start, 27 < strlen(iban_start) ? 27 : strlen(iban_start));
|
p->normalized_iban = normalized_iban;
|
||||||
p->country_code = g_strndup(p->normalized_iban, 2);
|
p->country_code = g_strndup(p->normalized_iban, 2);
|
||||||
p->is_iban_valid = bank_proposal_validate_iban(p->normalized_iban);
|
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->verification_status = g_strdup("proposed");
|
||||||
p->provenance_kind = g_strdup("ocr");
|
p->provenance_kind = g_strdup("ocr");
|
||||||
p->evidence_id = evidence_id != NULL ? g_strdup(evidence_id) : NULL;
|
p->evidence_id = evidence_id != NULL ? g_strdup(evidence_id) : NULL;
|
||||||
|
|
@ -197,6 +315,37 @@ BankProposal *bank_proposal_analyze_text(const char *raw_text, const char *evide
|
||||||
bank_proposal_derive_french_rib(p);
|
bank_proposal_derive_french_rib(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
g_string_free(clean, TRUE);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
75
src/core/document_analysis.c
Normal file
75
src/core/document_analysis.c
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* @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";
|
||||||
|
}
|
||||||
101
src/core/document_file_analysis.c
Normal file
101
src/core/document_file_analysis.c
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
327
src/core/document_tool_runner.c
Normal file
327
src/core/document_tool_runner.c
Normal file
|
|
@ -0,0 +1,327 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
* @brief Analyse locale et non destructive des en-têtes d'un fichier EML.
|
* @brief Analyse locale et non destructive des en-têtes d'un fichier EML.
|
||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
#include "core/eml_analyzer.h"
|
#include "core/eml_analyzer.h"
|
||||||
|
#include <gio/gio.h>
|
||||||
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#define EML_ANALYZER_MAX_FILE_SIZE (25U * 1024U * 1024U)
|
#define EML_ANALYZER_MAX_FILE_SIZE (25U * 1024U * 1024U)
|
||||||
#define EML_ANALYZER_MAX_HEADER_SIZE (2U * 1024U * 1024U)
|
#define EML_ANALYZER_MAX_HEADER_SIZE (2U * 1024U * 1024U)
|
||||||
|
|
@ -14,8 +16,132 @@ struct EmlAnalysis
|
||||||
GPtrArray *ips;
|
GPtrArray *ips;
|
||||||
GPtrArray *sender_ips;
|
GPtrArray *sender_ips;
|
||||||
GPtrArray *destination_ips;
|
GPtrArray *destination_ips;
|
||||||
|
GPtrArray *observations;
|
||||||
char *raw_headers;
|
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. */
|
/** @brief Libère un tableau de valeurs d'en-tête. */
|
||||||
static void eml_analyzer_values_free(gpointer data)
|
static void eml_analyzer_values_free(gpointer data)
|
||||||
{
|
{
|
||||||
|
|
@ -54,6 +180,129 @@ static void eml_analyzer_extract_regex(GRegex *regex, const char *text,
|
||||||
}
|
}
|
||||||
g_match_info_free(matches);
|
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. */
|
/** @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,
|
static void eml_analyzer_extract_received_part(GRegex *part_regex,
|
||||||
GRegex *ip_regex, const char *received, GPtrArray *values)
|
GRegex *ip_regex, const char *received, GPtrArray *values)
|
||||||
|
|
@ -100,6 +349,7 @@ EmlAnalysis *eml_analyzer_analyze_file(const char *file_path, GError **error)
|
||||||
char *utf8 = NULL, **lines = NULL, *current_name = NULL;
|
char *utf8 = NULL, **lines = NULL, *current_name = NULL;
|
||||||
GString *current_value = NULL;
|
GString *current_value = NULL;
|
||||||
GRegex *email_regex = NULL, *domain_regex = NULL, *ip_regex = NULL;
|
GRegex *email_regex = NULL, *domain_regex = NULL, *ip_regex = NULL;
|
||||||
|
GRegex *ipv6_regex = NULL;
|
||||||
GRegex *received_from_regex = NULL, *received_by_regex = NULL;
|
GRegex *received_from_regex = NULL, *received_by_regex = NULL;
|
||||||
g_return_val_if_fail(error == NULL || *error == NULL, NULL);
|
g_return_val_if_fail(error == NULL || *error == NULL, NULL);
|
||||||
if (file_path == NULL || file_path[0] == '\0')
|
if (file_path == NULL || file_path[0] == '\0')
|
||||||
|
|
@ -127,6 +377,8 @@ EmlAnalysis *eml_analyzer_analyze_file(const char *file_path, GError **error)
|
||||||
analysis->ips = g_ptr_array_new_with_free_func(g_free);
|
analysis->ips = g_ptr_array_new_with_free_func(g_free);
|
||||||
analysis->sender_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->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);
|
analysis->raw_headers = g_utf8_make_valid(data, (gssize) header_size);
|
||||||
utf8 = g_strdup(analysis->raw_headers); lines = g_strsplit(utf8, "\n", -1);
|
utf8 = g_strdup(analysis->raw_headers); lines = g_strsplit(utf8, "\n", -1);
|
||||||
current_value = g_string_new(NULL);
|
current_value = g_string_new(NULL);
|
||||||
|
|
@ -146,13 +398,42 @@ EmlAnalysis *eml_analyzer_analyze_file(const char *file_path, GError **error)
|
||||||
if (current_name != NULL) eml_analyzer_add_header(analysis,
|
if (current_name != NULL) eml_analyzer_add_header(analysis,
|
||||||
current_name, current_value->str);
|
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);
|
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("(?:@|[.\\s<([])([A-Za-z0-9-]+(?:\\.[A-Za-z0-9-]+)+)", 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);
|
||||||
ip_regex = g_regex_new("(?:^|[^0-9])([0-9]{1,3}(?:\\.[0-9]{1,3}){3})(?:[^0-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_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);
|
received_by_regex = g_regex_new("(?i)\\bby\\b(.*?)(?:\\bwith\\b|;|$)", 0, 0, NULL);
|
||||||
eml_analyzer_extract_regex(email_regex, analysis->raw_headers, analysis->emails, TRUE);
|
static const struct { const char *header; const char *role; } email_headers[] = {
|
||||||
eml_analyzer_extract_regex(domain_regex, analysis->raw_headers, analysis->domains, TRUE);
|
{ "from", "from" }, { "sender", "sender" },
|
||||||
eml_analyzer_extract_regex(ip_regex, analysis->raw_headers, analysis->ips, FALSE);
|
{ "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);
|
||||||
const GPtrArray *received_values = eml_analysis_get_header_values(
|
const GPtrArray *received_values = eml_analysis_get_header_values(
|
||||||
analysis, "received");
|
analysis, "received");
|
||||||
for (guint i = 0; received_values != NULL && i < received_values->len; i++)
|
for (guint i = 0; received_values != NULL && i < received_values->len; i++)
|
||||||
|
|
@ -164,9 +445,13 @@ EmlAnalysis *eml_analyzer_analyze_file(const char *file_path, GError **error)
|
||||||
eml_analyzer_extract_received_part(received_by_regex, ip_regex,
|
eml_analyzer_extract_received_part(received_by_regex, ip_regex,
|
||||||
received, analysis->destination_ips);
|
received, analysis->destination_ips);
|
||||||
}
|
}
|
||||||
|
analysis->date_utc = eml_analyzer_normalize_date(
|
||||||
|
eml_analysis_get_first_header(analysis, "date")
|
||||||
|
);
|
||||||
cleanup:
|
cleanup:
|
||||||
g_clear_pointer(&email_regex, g_regex_unref); g_clear_pointer(&domain_regex, g_regex_unref);
|
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(¤t_name, g_free);
|
g_clear_pointer(&ip_regex, g_regex_unref); g_clear_pointer(¤t_name, g_free);
|
||||||
|
g_clear_pointer(&ipv6_regex, g_regex_unref);
|
||||||
g_clear_pointer(&received_from_regex, g_regex_unref);
|
g_clear_pointer(&received_from_regex, g_regex_unref);
|
||||||
g_clear_pointer(&received_by_regex, g_regex_unref);
|
g_clear_pointer(&received_by_regex, g_regex_unref);
|
||||||
if (current_value != NULL) g_string_free(current_value, TRUE);
|
if (current_value != NULL) g_string_free(current_value, TRUE);
|
||||||
|
|
@ -180,7 +465,8 @@ void eml_analysis_free(EmlAnalysis *analysis)
|
||||||
g_ptr_array_unref(analysis->domains); g_ptr_array_unref(analysis->ips);
|
g_ptr_array_unref(analysis->domains); g_ptr_array_unref(analysis->ips);
|
||||||
g_ptr_array_unref(analysis->sender_ips);
|
g_ptr_array_unref(analysis->sender_ips);
|
||||||
g_ptr_array_unref(analysis->destination_ips);
|
g_ptr_array_unref(analysis->destination_ips);
|
||||||
g_free(analysis->raw_headers); g_free(analysis);
|
g_ptr_array_unref(analysis->observations);
|
||||||
|
g_free(analysis->raw_headers); g_free(analysis->date_utc); g_free(analysis);
|
||||||
}
|
}
|
||||||
const GPtrArray *eml_analysis_get_header_values(const EmlAnalysis *analysis,
|
const GPtrArray *eml_analysis_get_header_values(const EmlAnalysis *analysis,
|
||||||
const char *name)
|
const char *name)
|
||||||
|
|
@ -198,7 +484,9 @@ 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_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_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_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_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 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_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; }
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
#include "dao/entity_dao.h"
|
#include "dao/entity_dao.h"
|
||||||
#include "dao/evidence_entity_dao.h"
|
#include "dao/evidence_entity_dao.h"
|
||||||
#include "database/transaction.h"
|
#include "database/transaction.h"
|
||||||
|
#include "database/statement.h"
|
||||||
#include "models/entity_record.h"
|
#include "models/entity_record.h"
|
||||||
|
|
||||||
EmlEntityProposal *eml_entity_proposal_new(const char *type, const char *value)
|
EmlEntityProposal *eml_entity_proposal_new(const char *type, const char *value)
|
||||||
|
|
@ -26,16 +27,40 @@ EmlEntityProposal *eml_entity_proposal_new_with_metadata(const char *type,
|
||||||
? verification_status : "proposed");
|
? verification_status : "proposed");
|
||||||
proposal->provenance_kind = g_strdup(provenance_kind != NULL
|
proposal->provenance_kind = g_strdup(provenance_kind != NULL
|
||||||
? provenance_kind : "header");
|
? 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 ||
|
if (proposal->type_identifier == NULL || proposal->value == NULL ||
|
||||||
proposal->verification_status == NULL || proposal->provenance_kind == NULL)
|
proposal->verification_status == NULL || proposal->provenance_kind == NULL)
|
||||||
{ eml_entity_proposal_free(proposal); return NULL; }
|
{ eml_entity_proposal_free(proposal); return NULL; }
|
||||||
return proposal;
|
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)
|
void eml_entity_proposal_free(EmlEntityProposal *proposal)
|
||||||
{
|
{
|
||||||
if (proposal == NULL) return;
|
if (proposal == NULL) return;
|
||||||
g_free(proposal->type_identifier); g_free(proposal->value);
|
g_free(proposal->type_identifier); g_free(proposal->value);
|
||||||
g_free(proposal->verification_status); g_free(proposal->provenance_kind);
|
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);
|
g_free(proposal);
|
||||||
}
|
}
|
||||||
/** @brief Recherche une entité existante avec le même type et la même valeur. */
|
/** @brief Recherche une entité existante avec le même type et la même valeur. */
|
||||||
|
|
@ -53,7 +78,8 @@ static const EntityRecord *eml_integration_find_existing(const GPtrArray *entiti
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
gboolean eml_integration_apply(Database *database, const char *evidence_identifier,
|
gboolean eml_integration_apply(Database *database, const char *evidence_identifier,
|
||||||
const GPtrArray *proposals, guint *out_created, guint *out_reused, GError **error)
|
const GPtrArray *proposals, guint *out_observations,
|
||||||
|
guint *out_created, guint *out_reused, GError **error)
|
||||||
{
|
{
|
||||||
EntityDao *entity_dao = NULL;
|
EntityDao *entity_dao = NULL;
|
||||||
EvidenceEntityDao *link_dao = NULL;
|
EvidenceEntityDao *link_dao = NULL;
|
||||||
|
|
@ -62,6 +88,7 @@ gboolean eml_integration_apply(Database *database, const char *evidence_identifi
|
||||||
char *timestamp = NULL;
|
char *timestamp = NULL;
|
||||||
guint created = 0;
|
guint created = 0;
|
||||||
guint reused = 0;
|
guint reused = 0;
|
||||||
|
guint observations = 0;
|
||||||
gboolean active = FALSE;
|
gboolean active = FALSE;
|
||||||
gboolean success = FALSE;
|
gboolean success = FALSE;
|
||||||
|
|
||||||
|
|
@ -71,6 +98,8 @@ gboolean eml_integration_apply(Database *database, const char *evidence_identifi
|
||||||
*out_created = 0;
|
*out_created = 0;
|
||||||
if (out_reused != NULL)
|
if (out_reused != NULL)
|
||||||
*out_reused = 0;
|
*out_reused = 0;
|
||||||
|
if (out_observations != NULL)
|
||||||
|
*out_observations = 0;
|
||||||
|
|
||||||
if (database == NULL || evidence_identifier == NULL || proposals == NULL || proposals->len == 0)
|
if (database == NULL || evidence_identifier == NULL || proposals == NULL || proposals->len == 0)
|
||||||
{
|
{
|
||||||
|
|
@ -84,6 +113,10 @@ gboolean eml_integration_apply(Database *database, const char *evidence_identifi
|
||||||
const EmlEntityProposal *proposal = g_ptr_array_index(
|
const EmlEntityProposal *proposal = g_ptr_array_index(
|
||||||
(GPtrArray *) proposals, i);
|
(GPtrArray *) proposals, i);
|
||||||
if (proposal == NULL ||
|
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(
|
!controlled_vocab_is_valid_verification_status(
|
||||||
proposal->verification_status) ||
|
proposal->verification_status) ||
|
||||||
!controlled_vocab_is_valid_provenance_kind(
|
!controlled_vocab_is_valid_provenance_kind(
|
||||||
|
|
@ -116,11 +149,21 @@ gboolean eml_integration_apply(Database *database, const char *evidence_identifi
|
||||||
for (guint i = 0; i < proposals->len; i++)
|
for (guint i = 0; i < proposals->len; i++)
|
||||||
{
|
{
|
||||||
EmlEntityProposal *proposal = g_ptr_array_index((GPtrArray *) proposals, 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 EntityRecord *existing = eml_integration_find_existing(entities, proposal);
|
||||||
const char *identifier = existing != NULL
|
const char *identifier = existing != NULL
|
||||||
? entity_record_get_identifier(existing) : NULL;
|
? entity_record_get_identifier(existing) : NULL;
|
||||||
char *new_identifier = NULL; EntityRecord *new_record = NULL;
|
char *new_identifier = NULL; EntityRecord *new_record = NULL;
|
||||||
gboolean linked = FALSE;
|
|
||||||
if (existing == NULL)
|
if (existing == NULL)
|
||||||
{
|
{
|
||||||
new_identifier = g_uuid_string_random(); identifier = new_identifier;
|
new_identifier = g_uuid_string_random(); identifier = new_identifier;
|
||||||
|
|
@ -129,21 +172,27 @@ gboolean eml_integration_apply(Database *database, const char *evidence_identifi
|
||||||
"Indicateur extrait des en-têtes de la preuve EML.", 50,
|
"Indicateur extrait des en-têtes de la preuve EML.", 50,
|
||||||
timestamp, timestamp, ENTITY_STATUS_ACTIVE, error);
|
timestamp, timestamp, ENTITY_STATUS_ACTIVE, error);
|
||||||
if (new_record == NULL || !entity_dao_insert(entity_dao, new_record, error))
|
if (new_record == NULL || !entity_dao_insert(entity_dao, new_record, error))
|
||||||
{ entity_record_free(new_record); g_free(new_identifier); goto cleanup; }
|
{ entity_record_free(new_record); g_free(observation_identifier);
|
||||||
|
g_free(new_identifier); goto cleanup; }
|
||||||
g_ptr_array_add(entities, new_record); new_record = NULL; created++;
|
g_ptr_array_add(entities, new_record); new_record = NULL; created++;
|
||||||
}
|
}
|
||||||
else reused++;
|
else reused++;
|
||||||
if (!evidence_entity_dao_exists(link_dao, evidence_identifier,
|
if (!evidence_entity_dao_add_source(link_dao, evidence_identifier,
|
||||||
identifier, &linked, error) ||
|
identifier, "eml_observation", observation_identifier,
|
||||||
(!linked && !evidence_entity_dao_link(link_dao,
|
timestamp, error))
|
||||||
evidence_identifier, identifier, error)))
|
{ g_free(observation_identifier); g_free(new_identifier); goto cleanup; }
|
||||||
{ 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);
|
||||||
g_free(new_identifier);
|
g_free(new_identifier);
|
||||||
}
|
}
|
||||||
if (!database_transaction_commit(database)) goto cleanup;
|
if (!database_transaction_commit(database)) goto cleanup;
|
||||||
active = FALSE; success = TRUE;
|
active = FALSE; success = TRUE;
|
||||||
if (out_created != NULL) *out_created = created;
|
if (out_created != NULL) *out_created = created;
|
||||||
if (out_reused != NULL) *out_reused = reused;
|
if (out_reused != NULL) *out_reused = reused;
|
||||||
|
if (out_observations != NULL) *out_observations = observations;
|
||||||
cleanup:
|
cleanup:
|
||||||
if (!success && active) database_transaction_rollback(database);
|
if (!success && active) database_transaction_rollback(database);
|
||||||
g_free(timestamp); g_clear_pointer(&now, g_date_time_unref);
|
g_free(timestamp); g_clear_pointer(&now, g_date_time_unref);
|
||||||
|
|
@ -151,3 +200,100 @@ cleanup:
|
||||||
evidence_entity_dao_free(link_dao); entity_dao_free(entity_dao);
|
evidence_entity_dao_free(link_dao); entity_dao_free(entity_dao);
|
||||||
return success;
|
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 n’est 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
|
|
@ -4,7 +4,6 @@
|
||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
#include "core/eml_pipeline_task.h"
|
#include "core/eml_pipeline_task.h"
|
||||||
#include "core/file_hash.h"
|
#include "core/file_hash.h"
|
||||||
#include "core/rib_ocr.h"
|
|
||||||
#include <gio/gio.h>
|
#include <gio/gio.h>
|
||||||
#include <glib.h>
|
#include <glib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
@ -14,6 +13,12 @@ typedef struct
|
||||||
char *eml_path;
|
char *eml_path;
|
||||||
char *processed_evidence_dir;
|
char *processed_evidence_dir;
|
||||||
char *evidence_id;
|
char *evidence_id;
|
||||||
|
char *exiftool;
|
||||||
|
char *tesseract;
|
||||||
|
char *pdfinfo;
|
||||||
|
char *pdftotext;
|
||||||
|
char *pdftoppm;
|
||||||
|
guint document_analysis_limit;
|
||||||
} EmlPipelineTaskData;
|
} EmlPipelineTaskData;
|
||||||
|
|
||||||
static void eml_pipeline_task_data_free(gpointer user_data)
|
static void eml_pipeline_task_data_free(gpointer user_data)
|
||||||
|
|
@ -24,6 +29,11 @@ static void eml_pipeline_task_data_free(gpointer user_data)
|
||||||
g_free(data->eml_path);
|
g_free(data->eml_path);
|
||||||
g_free(data->processed_evidence_dir);
|
g_free(data->processed_evidence_dir);
|
||||||
g_free(data->evidence_id);
|
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);
|
g_free(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,6 +47,8 @@ void eml_pipeline_result_free(EmlPipelineResult *res)
|
||||||
eml_mime_result_free(res->mime_result);
|
eml_mime_result_free(res->mime_result);
|
||||||
if (res->bank_proposals != NULL)
|
if (res->bank_proposals != NULL)
|
||||||
g_ptr_array_unref(res->bank_proposals);
|
g_ptr_array_unref(res->bank_proposals);
|
||||||
|
if (res->document_analyses != NULL)
|
||||||
|
g_ptr_array_unref(res->document_analyses);
|
||||||
if (res->warnings != NULL)
|
if (res->warnings != NULL)
|
||||||
g_ptr_array_unref(res->warnings);
|
g_ptr_array_unref(res->warnings);
|
||||||
g_free(res);
|
g_free(res);
|
||||||
|
|
@ -77,23 +89,57 @@ static gboolean eml_pipeline_task_worker(BackgroundTask *task,
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
EmlMimeResult *mime_res = eml_mime_extract_attachments(data->eml_path, target_dir, error);
|
EmlMimeResult *mime_res = eml_mime_extract_attachments_cancellable(
|
||||||
|
data->eml_path,
|
||||||
|
target_dir,
|
||||||
|
cancellable,
|
||||||
|
error
|
||||||
|
);
|
||||||
g_free(target_dir);
|
g_free(target_dir);
|
||||||
|
|
||||||
if (mime_res == NULL)
|
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) */
|
/* 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 = g_new0(EmlMimeResult, 1);
|
||||||
mime_res->attachments = g_ptr_array_new_with_free_func((GDestroyNotify) eml_attachment_free);
|
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);
|
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."));
|
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.75, "Analyse OCR et détection bancaire...");
|
background_task_report_progress(task, 0.65,
|
||||||
|
"Analyse des pièces jointes avec ExifTool…");
|
||||||
GPtrArray *bank_proposals = g_ptr_array_new_with_free_func((GDestroyNotify) bank_proposal_free);
|
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++)
|
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);
|
EmlAttachment *att = g_ptr_array_index(mime_res->attachments, i);
|
||||||
if (att->extracted_path == NULL)
|
if (att->extracted_path == NULL)
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -113,13 +159,51 @@ static gboolean eml_pipeline_task_worker(BackgroundTask *task,
|
||||||
g_free(content);
|
g_free(content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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"))
|
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)
|
||||||
{
|
{
|
||||||
char *ocr_text = NULL;
|
background_task_report_progress(task, 0.78,
|
||||||
char *ocr_version = NULL;
|
g_strcmp0(att->detected_mime, "application/pdf") == 0
|
||||||
(void) rib_ocr_extract_text(att->extracted_path, &ocr_text,
|
? "Extraction du texte PDF ou OCR…"
|
||||||
&ocr_version, NULL);
|
: "Reconnaissance OCR de la pièce jointe…");
|
||||||
g_free(ocr_version);
|
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;
|
||||||
if (ocr_text != NULL)
|
if (ocr_text != NULL)
|
||||||
{
|
{
|
||||||
BankProposal *bp = bank_proposal_analyze_text(ocr_text, data->evidence_id);
|
BankProposal *bp = bank_proposal_analyze_text(ocr_text, data->evidence_id);
|
||||||
|
|
@ -128,22 +212,36 @@ static gboolean eml_pipeline_task_worker(BackgroundTask *task,
|
||||||
bp->extraction_id = g_strdup(att->part_index);
|
bp->extraction_id = g_strdup(att->part_index);
|
||||||
g_ptr_array_add(bank_proposals, bp);
|
g_ptr_array_add(bank_proposals, bp);
|
||||||
}
|
}
|
||||||
g_free(ocr_text);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
background_task_report_progress(task, 1.0, "Analyse EML terminée avec succès.");
|
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…");
|
||||||
|
|
||||||
EmlPipelineResult *res = g_new0(EmlPipelineResult, 1);
|
EmlPipelineResult *res = g_new0(EmlPipelineResult, 1);
|
||||||
res->analysis = analysis;
|
res->analysis = analysis;
|
||||||
res->mime_result = mime_res;
|
res->mime_result = mime_res;
|
||||||
res->bank_proposals = bank_proposals;
|
res->bank_proposals = bank_proposals;
|
||||||
res->warnings = g_ptr_array_new_with_free_func(g_free);
|
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));
|
||||||
|
|
||||||
if (out_result != NULL)
|
if (out_result != NULL)
|
||||||
*out_result = res;
|
*out_result = res;
|
||||||
|
|
||||||
|
background_task_report_progress(task, 1.0,
|
||||||
|
"Analyse EML terminée avec succès.");
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -151,13 +249,50 @@ BackgroundTask *eml_pipeline_task_new(const char *eml_path,
|
||||||
const char *processed_evidence_dir,
|
const char *processed_evidence_dir,
|
||||||
const char *evidence_id)
|
const char *evidence_id)
|
||||||
{
|
{
|
||||||
if (eml_path == NULL || processed_evidence_dir == NULL)
|
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)
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
EmlPipelineTaskData *data = g_new0(EmlPipelineTaskData, 1);
|
EmlPipelineTaskData *data = g_new0(EmlPipelineTaskData, 1);
|
||||||
data->eml_path = g_strdup(eml_path);
|
data->eml_path = g_strdup(eml_path);
|
||||||
data->processed_evidence_dir = g_strdup(processed_evidence_dir);
|
data->processed_evidence_dir = g_strdup(processed_evidence_dir);
|
||||||
data->evidence_id = g_strdup(evidence_id);
|
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(
|
BackgroundTask *task = background_task_new(
|
||||||
"Analyse du message EML et de ses pièces jointes");
|
"Analyse du message EML et de ses pièces jointes");
|
||||||
|
|
@ -175,3 +310,43 @@ BackgroundTask *eml_pipeline_task_new(const char *eml_path,
|
||||||
}
|
}
|
||||||
return task;
|
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 l’e-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;
|
||||||
|
}
|
||||||
|
|
|
||||||
279
src/core/exiftool_analysis.c
Normal file
279
src/core/exiftool_analysis.c
Normal file
|
|
@ -0,0 +1,279 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
@ -237,7 +237,6 @@ static gboolean extraction_drop_service_attach_in_transaction(
|
||||||
{
|
{
|
||||||
EvidenceEntityDao *link_dao = NULL;
|
EvidenceEntityDao *link_dao = NULL;
|
||||||
char *evidence_identifier = NULL;
|
char *evidence_identifier = NULL;
|
||||||
gboolean exists = FALSE;
|
|
||||||
gboolean success = FALSE;
|
gboolean success = FALSE;
|
||||||
|
|
||||||
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
|
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
|
||||||
|
|
@ -254,10 +253,8 @@ static gboolean extraction_drop_service_attach_in_transaction(
|
||||||
if (evidence_identifier == NULL) goto cleanup;
|
if (evidence_identifier == NULL) goto cleanup;
|
||||||
link_dao = evidence_entity_dao_new(database, error);
|
link_dao = evidence_entity_dao_new(database, error);
|
||||||
if (link_dao == NULL ||
|
if (link_dao == NULL ||
|
||||||
!evidence_entity_dao_exists(link_dao, evidence_identifier,
|
!evidence_entity_dao_link(link_dao, evidence_identifier,
|
||||||
entity_identifier, &exists, error) ||
|
entity_identifier, error)) goto cleanup;
|
||||||
(!exists && !evidence_entity_dao_link(link_dao, evidence_identifier,
|
|
||||||
entity_identifier, error))) goto cleanup;
|
|
||||||
success = TRUE;
|
success = TRUE;
|
||||||
cleanup:
|
cleanup:
|
||||||
evidence_entity_dao_free(link_dao);
|
evidence_entity_dao_free(link_dao);
|
||||||
|
|
|
||||||
98
src/core/ocr_analysis.c
Normal file
98
src/core/ocr_analysis.c
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
327
src/core/pdf_analysis.c
Normal file
327
src/core/pdf_analysis.c
Normal file
|
|
@ -0,0 +1,327 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
@ -52,7 +52,7 @@ static const char *const evidence_entity_dao_association_exists_sql =
|
||||||
* @brief Requête créant une association.
|
* @brief Requête créant une association.
|
||||||
*/
|
*/
|
||||||
static const char *const evidence_entity_dao_link_sql =
|
static const char *const evidence_entity_dao_link_sql =
|
||||||
"INSERT INTO preuve_entites"
|
"INSERT OR IGNORE INTO preuve_entites"
|
||||||
"("
|
"("
|
||||||
" preuve_id,"
|
" preuve_id,"
|
||||||
" entite_id"
|
" entite_id"
|
||||||
|
|
@ -71,6 +71,36 @@ static const char *const evidence_entity_dao_unlink_sql =
|
||||||
"WHERE preuve_id = ? "
|
"WHERE preuve_id = ? "
|
||||||
"AND entite_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.
|
* @brief Requête listant les entités liées à une preuve.
|
||||||
*/
|
*/
|
||||||
|
|
@ -801,8 +831,9 @@ gboolean evidence_entity_dao_link(
|
||||||
gboolean entity_exists =
|
gboolean entity_exists =
|
||||||
FALSE;
|
FALSE;
|
||||||
|
|
||||||
gboolean association_exists =
|
GDateTime *now = NULL;
|
||||||
FALSE;
|
char *created_at = NULL;
|
||||||
|
gboolean success = FALSE;
|
||||||
|
|
||||||
g_return_val_if_fail(
|
g_return_val_if_fail(
|
||||||
error == NULL || *error == NULL,
|
error == NULL || *error == NULL,
|
||||||
|
|
@ -865,39 +896,20 @@ gboolean evidence_entity_dao_link(
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!evidence_entity_dao_query_exists(
|
now = g_date_time_new_now_utc();
|
||||||
evidence_entity_dao,
|
created_at = now != NULL
|
||||||
evidence_entity_dao_association_exists_sql,
|
? g_date_time_format(now, "%Y-%m-%dT%H:%M:%SZ") : NULL;
|
||||||
evidence_identifier,
|
if (created_at == NULL)
|
||||||
entity_identifier,
|
evidence_entity_dao_set_error_literal(error,
|
||||||
&association_exists,
|
EVIDENCE_ENTITY_DAO_ERROR_MEMORY,
|
||||||
error
|
"Impossible de dater l'association preuve-entité.");
|
||||||
))
|
else
|
||||||
{
|
success = evidence_entity_dao_add_source(evidence_entity_dao,
|
||||||
return FALSE;
|
evidence_identifier, entity_identifier, "manual", NULL,
|
||||||
}
|
created_at, error);
|
||||||
|
g_free(created_at);
|
||||||
if (association_exists)
|
g_clear_pointer(&now, g_date_time_unref);
|
||||||
{
|
return success;
|
||||||
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(
|
gboolean evidence_entity_dao_unlink(
|
||||||
|
|
@ -948,16 +960,103 @@ gboolean evidence_entity_dao_unlink(
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
return evidence_entity_dao_execute_pair_statement(
|
return evidence_entity_dao_remove_source(evidence_entity_dao,
|
||||||
evidence_entity_dao,
|
evidence_identifier, entity_identifier, "manual", NULL, NULL, error);
|
||||||
evidence_entity_dao_unlink_sql,
|
}
|
||||||
evidence_identifier,
|
|
||||||
entity_identifier,
|
gboolean evidence_entity_dao_add_source(
|
||||||
"Impossible de préparer la suppression de l'association",
|
EvidenceEntityDao *dao, const char *evidence_identifier,
|
||||||
"Impossible de lier les identifiants de l'association",
|
const char *entity_identifier, const char *source_kind,
|
||||||
"Impossible de supprimer l'association",
|
const char *source_uuid, const char *created_at, GError **error)
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
gboolean evidence_entity_dao_exists(
|
gboolean evidence_entity_dao_exists(
|
||||||
|
|
@ -1067,3 +1166,218 @@ GPtrArray *evidence_entity_dao_list_evidence_identifiers(
|
||||||
error
|
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 d’enregistrer l’observation 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 l’observation");
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,12 @@
|
||||||
/**
|
/**
|
||||||
* @brief Version actuelle du schéma SQLite.
|
* @brief Version actuelle du schéma SQLite.
|
||||||
*/
|
*/
|
||||||
#define DATABASE_SCHEMA_VERSION_CURRENT 10
|
#define DATABASE_SCHEMA_VERSION_CURRENT 13
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Version actuelle sous forme textuelle pour metadata.
|
* @brief Version actuelle sous forme textuelle pour metadata.
|
||||||
*/
|
*/
|
||||||
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "10"
|
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "13"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Nom de l'application enregistré dans les métadonnées.
|
* @brief Nom de l'application enregistré dans les métadonnées.
|
||||||
|
|
@ -812,6 +812,51 @@ rollback:
|
||||||
return false;
|
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 d’annuler 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 d’annuler 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 d’annuler la migration SQLite V12 vers V13.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Garantit atomiquement la présence des extensions du schéma courant.
|
* @brief Garantit atomiquement la présence des extensions du schéma courant.
|
||||||
*/
|
*/
|
||||||
|
|
@ -1041,6 +1086,18 @@ bool database_migrate_to_latest(
|
||||||
if (!database_migrate_v9_to_v10(database)) return false;
|
if (!database_migrate_v9_to_v10(database)) return false;
|
||||||
schema_version = 10;
|
schema_version = 10;
|
||||||
break;
|
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:
|
default:
|
||||||
database_set_error(
|
database_set_error(
|
||||||
|
|
|
||||||
|
|
@ -353,6 +353,24 @@ bool schema_install_v10(Database *database)
|
||||||
"la migration SQLite V10");
|
"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(
|
bool schema_ensure_current(
|
||||||
Database *database
|
Database *database
|
||||||
)
|
)
|
||||||
|
|
|
||||||
11
src/models/evidence_observation.c
Normal file
11
src/models/evidence_observation.c
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
#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);
|
||||||
|
}
|
||||||
|
|
@ -5,8 +5,54 @@
|
||||||
#include "views/eml_analysis_dialog.h"
|
#include "views/eml_analysis_dialog.h"
|
||||||
#include "widgets/controlled_vocab_dropdown.h"
|
#include "widgets/controlled_vocab_dropdown.h"
|
||||||
typedef struct { GtkWindow *window; GtkWidget *proposals_box;
|
typedef struct { GtkWindow *window; GtkWidget *proposals_box;
|
||||||
|
GtkWidget *summary_label;
|
||||||
EmlAnalysisDialogCallback callback; gpointer user_data; gboolean completed;
|
EmlAnalysisDialogCallback callback; gpointer user_data; gboolean completed;
|
||||||
} EmlAnalysisDialogState;
|
} 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. */
|
/** @brief Libère l'état de révision. */
|
||||||
static void eml_analysis_dialog_state_free(gpointer data) { g_free(data); }
|
static void eml_analysis_dialog_state_free(gpointer data) { g_free(data); }
|
||||||
/** @brief Signale une annulation une seule fois. */
|
/** @brief Signale une annulation une seule fois. */
|
||||||
|
|
@ -30,7 +76,7 @@ static void eml_analysis_dialog_on_integrate(GtkButton *button, gpointer data)
|
||||||
for (GtkWidget *child = gtk_widget_get_first_child(state->proposals_box);
|
for (GtkWidget *child = gtk_widget_get_first_child(state->proposals_box);
|
||||||
child != NULL; child = gtk_widget_get_next_sibling(child))
|
child != NULL; child = gtk_widget_get_next_sibling(child))
|
||||||
{
|
{
|
||||||
GtkWidget *check = GTK_IS_BOX(child) ? gtk_widget_get_first_child(child) : NULL;
|
GtkWidget *check = g_object_get_data(G_OBJECT(child), "eml-keep");
|
||||||
if (check != NULL && GTK_IS_CHECK_BUTTON(check) &&
|
if (check != NULL && GTK_IS_CHECK_BUTTON(check) &&
|
||||||
gtk_check_button_get_active(GTK_CHECK_BUTTON(check)))
|
gtk_check_button_get_active(GTK_CHECK_BUTTON(check)))
|
||||||
{
|
{
|
||||||
|
|
@ -39,10 +85,23 @@ static void eml_analysis_dialog_on_integrate(GtkButton *button, gpointer data)
|
||||||
GtkWidget *status = g_object_get_data(G_OBJECT(child), "eml-status");
|
GtkWidget *status = g_object_get_data(G_OBJECT(child), "eml-status");
|
||||||
GtkWidget *provenance = g_object_get_data(G_OBJECT(child),
|
GtkWidget *provenance = g_object_get_data(G_OBJECT(child),
|
||||||
"eml-provenance");
|
"eml-provenance");
|
||||||
g_ptr_array_add(selected, eml_entity_proposal_new_with_metadata(
|
const char *raw = g_object_get_data(G_OBJECT(child), "eml-raw");
|
||||||
type, value,
|
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,
|
||||||
controlled_vocab_dropdown_get_selected_code(status),
|
controlled_vocab_dropdown_get_selected_code(status),
|
||||||
controlled_vocab_dropdown_get_selected_code(provenance)));
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (selected->len == 0)
|
if (selected->len == 0)
|
||||||
|
|
@ -52,6 +111,54 @@ static void eml_analysis_dialog_on_integrate(GtkButton *button, gpointer data)
|
||||||
else g_ptr_array_unref(selected);
|
else g_ptr_array_unref(selected);
|
||||||
gtk_window_close(state->window);
|
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. */
|
/** @brief Ajoute les propositions d'un type sous forme de cases décochées. */
|
||||||
static void eml_analysis_dialog_add_proposals(GtkWidget *box,
|
static void eml_analysis_dialog_add_proposals(GtkWidget *box,
|
||||||
const char *type, const char *label, const GPtrArray *values)
|
const char *type, const char *label, const GPtrArray *values)
|
||||||
|
|
@ -61,7 +168,12 @@ static void eml_analysis_dialog_add_proposals(GtkWidget *box,
|
||||||
const char *value = g_ptr_array_index((GPtrArray *) values, i);
|
const char *value = g_ptr_array_index((GPtrArray *) values, i);
|
||||||
char *text = g_strdup_printf("%s : %s", label, value);
|
char *text = g_strdup_printf("%s : %s", label, value);
|
||||||
GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||||
GtkWidget *check = gtk_check_button_new_with_label(text);
|
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(
|
GtkWidget *status = controlled_vocab_dropdown_new(
|
||||||
CONTROLLED_VOCAB_VERIFICATION_STATUS, "proposed");
|
CONTROLLED_VOCAB_VERIFICATION_STATUS, "proposed");
|
||||||
GtkWidget *provenance = controlled_vocab_dropdown_new(
|
GtkWidget *provenance = controlled_vocab_dropdown_new(
|
||||||
|
|
@ -71,8 +183,12 @@ 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_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-status", status);
|
||||||
g_object_set_data(G_OBJECT(row), "eml-provenance", provenance);
|
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_widget_set_hexpand(check, TRUE);
|
||||||
gtk_box_append(GTK_BOX(row), check);
|
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), status);
|
||||||
gtk_box_append(GTK_BOX(row), provenance);
|
gtk_box_append(GTK_BOX(row), provenance);
|
||||||
gtk_box_append(GTK_BOX(box), row); g_free(text);
|
gtk_box_append(GTK_BOX(box), row); g_free(text);
|
||||||
|
|
@ -162,17 +278,13 @@ void eml_analysis_dialog_present(GtkWindow *parent,
|
||||||
destination_ips);
|
destination_ips);
|
||||||
gtk_box_append(GTK_BOX(content), grid);
|
gtk_box_append(GTK_BOX(content), grid);
|
||||||
gtk_box_append(GTK_BOX(content), gtk_label_new(
|
gtk_box_append(GTK_BOX(content), gtk_label_new(
|
||||||
"Sélection explicite des entités à intégrer"));
|
"Choisir les observations à conserver ; la promotion vers le graphe "
|
||||||
|
"est une action séparée."));
|
||||||
state->proposals_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
state->proposals_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
||||||
eml_analysis_dialog_add_proposals(state->proposals_box, "email_address",
|
eml_analysis_dialog_add_observations(state->proposals_box,
|
||||||
"Email", eml_analysis_get_email_addresses(analysis));
|
eml_analysis_get_observations(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);
|
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)"));
|
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);
|
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);
|
gtk_text_view_set_monospace(GTK_TEXT_VIEW(raw), TRUE);
|
||||||
|
|
@ -206,3 +318,230 @@ void eml_analysis_dialog_present(GtkWindow *parent,
|
||||||
g_free(received); g_free(emails); g_free(domains);
|
g_free(received); g_free(emails); g_free(domains);
|
||||||
g_free(sender_ips); g_free(destination_ips);
|
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 l’analyse 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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1995,6 +1995,31 @@ 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,
|
void main_window_set_evidence_preview(MainWindow *main_window,
|
||||||
const char *file_path, const char *display_name)
|
const char *file_path, const char *display_name)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,7 @@ struct Workspace
|
||||||
GtkWidget *evidence_internal_name_label;
|
GtkWidget *evidence_internal_name_label;
|
||||||
GtkWidget *evidence_identifier_label;
|
GtkWidget *evidence_identifier_label;
|
||||||
GtkWidget *evidence_sha256_label;
|
GtkWidget *evidence_sha256_label;
|
||||||
|
GtkWidget *evidence_observations_box;
|
||||||
GtkWidget *verify_evidence_button;
|
GtkWidget *verify_evidence_button;
|
||||||
GtkWidget *edit_evidence_button;
|
GtkWidget *edit_evidence_button;
|
||||||
GtkWidget *analyze_eml_button;
|
GtkWidget *analyze_eml_button;
|
||||||
|
|
@ -123,6 +124,8 @@ struct Workspace
|
||||||
gpointer extract_metadata_user_data;
|
gpointer extract_metadata_user_data;
|
||||||
WorkspaceRecoverPdfPasswordCallback recover_pdf_password_callback;
|
WorkspaceRecoverPdfPasswordCallback recover_pdf_password_callback;
|
||||||
gpointer recover_pdf_password_user_data;
|
gpointer recover_pdf_password_user_data;
|
||||||
|
WorkspaceObservationRemoveCallback observation_remove_callback;
|
||||||
|
gpointer observation_remove_user_data;
|
||||||
|
|
||||||
WorkspaceGraphNodeMovedCallback
|
WorkspaceGraphNodeMovedCallback
|
||||||
graph_node_moved_callback;
|
graph_node_moved_callback;
|
||||||
|
|
@ -1598,11 +1601,11 @@ Workspace *workspace_new(void)
|
||||||
G_CALLBACK(workspace_on_edit_evidence_clicked), workspace);
|
G_CALLBACK(workspace_on_edit_evidence_clicked), workspace);
|
||||||
gtk_box_append(GTK_BOX(evidence_content), workspace->edit_evidence_button);
|
gtk_box_append(GTK_BOX(evidence_content), workspace->edit_evidence_button);
|
||||||
workspace->analyze_eml_button = gtk_button_new_with_label(
|
workspace->analyze_eml_button = gtk_button_new_with_label(
|
||||||
"Analyser les en-têtes EML");
|
"Analyser l’e-mail");
|
||||||
gtk_widget_set_halign(workspace->analyze_eml_button, GTK_ALIGN_START);
|
gtk_widget_set_halign(workspace->analyze_eml_button, GTK_ALIGN_START);
|
||||||
gtk_widget_set_sensitive(workspace->analyze_eml_button, FALSE);
|
gtk_widget_set_sensitive(workspace->analyze_eml_button, FALSE);
|
||||||
gtk_widget_set_tooltip_text(workspace->analyze_eml_button,
|
gtk_widget_set_tooltip_text(workspace->analyze_eml_button,
|
||||||
"Créer une copie vérifiée puis analyser localement ses en-têtes");
|
"Vérifier la preuve puis lancer l’analyse complète en arrière-plan");
|
||||||
g_signal_connect(workspace->analyze_eml_button, "clicked",
|
g_signal_connect(workspace->analyze_eml_button, "clicked",
|
||||||
G_CALLBACK(workspace_on_analyze_eml_clicked), workspace);
|
G_CALLBACK(workspace_on_analyze_eml_clicked), workspace);
|
||||||
gtk_box_append(GTK_BOX(evidence_content), workspace->analyze_eml_button);
|
gtk_box_append(GTK_BOX(evidence_content), workspace->analyze_eml_button);
|
||||||
|
|
@ -1746,6 +1749,15 @@ Workspace *workspace_new(void)
|
||||||
10,
|
10,
|
||||||
"SHA-256"
|
"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 ||
|
if (workspace->evidence_type_label == NULL ||
|
||||||
workspace->evidence_integrity_label == NULL ||
|
workspace->evidence_integrity_label == NULL ||
|
||||||
|
|
@ -1757,7 +1769,8 @@ Workspace *workspace_new(void)
|
||||||
workspace->evidence_relative_path_label == NULL ||
|
workspace->evidence_relative_path_label == NULL ||
|
||||||
workspace->evidence_internal_name_label == NULL ||
|
workspace->evidence_internal_name_label == NULL ||
|
||||||
workspace->evidence_identifier_label == NULL ||
|
workspace->evidence_identifier_label == NULL ||
|
||||||
workspace->evidence_sha256_label == NULL)
|
workspace->evidence_sha256_label == NULL ||
|
||||||
|
workspace->evidence_observations_box == NULL)
|
||||||
{
|
{
|
||||||
workspace_free(
|
workspace_free(
|
||||||
workspace
|
workspace
|
||||||
|
|
@ -2576,6 +2589,82 @@ Workspace *workspace_new(void)
|
||||||
return workspace;
|
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(
|
GtkWidget *workspace_get_widget(
|
||||||
const Workspace *workspace
|
const Workspace *workspace
|
||||||
)
|
)
|
||||||
|
|
@ -2950,6 +3039,15 @@ 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,
|
void workspace_set_evidence_preview(Workspace *workspace,
|
||||||
const char *file_path, const char *display_name)
|
const char *file_path, const char *display_name)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
158
tests/fake_document_tool.c
Normal file
158
tests/fake_document_tool.c
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
38
tests/fixtures/eml/manual_smoke_test.eml
vendored
Normal file
38
tests/fixtures/eml/manual_smoke_test.eml
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
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--
|
||||||
|
|
@ -47,6 +47,33 @@ static void test_french_rib_derivation(void)
|
||||||
bank_proposal_free(proposal);
|
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)
|
int main(int argc, char **argv)
|
||||||
{
|
{
|
||||||
|
|
@ -54,5 +81,6 @@ int main(int argc, char **argv)
|
||||||
g_test_add_func("/bank-proposal/iban-validation", test_iban_validation);
|
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/bic-validation", test_bic_validation);
|
||||||
g_test_add_func("/bank-proposal/french-rib-derivation", test_french_rib_derivation);
|
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();
|
return g_test_run();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -525,7 +525,7 @@ static void test_database_initialize_valid_database(void)
|
||||||
"FROM investigation;"
|
"FROM investigation;"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert(strcmp(schema_version, "10") == 0);
|
assert(strcmp(schema_version, "13") == 0);
|
||||||
test_database_assert_table_exists(database, "bank_account_entities");
|
test_database_assert_table_exists(database, "bank_account_entities");
|
||||||
test_database_assert_table_exists(database, "relation_types");
|
test_database_assert_table_exists(database, "relation_types");
|
||||||
test_database_assert_table_exists(database, "graph_viewport");
|
test_database_assert_table_exists(database, "graph_viewport");
|
||||||
|
|
@ -992,7 +992,7 @@ static void test_database_migrate_v1_to_v2(void)
|
||||||
assert(
|
assert(
|
||||||
strcmp(
|
strcmp(
|
||||||
schema_version,
|
schema_version,
|
||||||
"10"
|
"13"
|
||||||
) == 0
|
) == 0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -1399,6 +1399,54 @@ 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)
|
int main(void)
|
||||||
{
|
{
|
||||||
test_database_initialize_valid_database();
|
test_database_initialize_valid_database();
|
||||||
|
|
@ -1406,6 +1454,7 @@ int main(void)
|
||||||
|
|
||||||
test_database_migrate_v1_to_v2();
|
test_database_migrate_v1_to_v2();
|
||||||
test_database_migration_rollback();
|
test_database_migration_rollback();
|
||||||
|
test_database_migrate_v12_to_v13_preserves_legacy_link();
|
||||||
|
|
||||||
test_database_initialize_invalid_parameters();
|
test_database_initialize_invalid_parameters();
|
||||||
test_database_initialize_missing_parent();
|
test_database_initialize_missing_parent();
|
||||||
|
|
|
||||||
160
tests/test_document_tool_runner.c
Normal file
160
tests/test_document_tool_runner.c
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* @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();
|
||||||
|
}
|
||||||
|
|
@ -16,7 +16,9 @@ static void test_eml_analyzer_headers(void)
|
||||||
"Reply-To: replies@reply.test\r\n"
|
"Reply-To: replies@reply.test\r\n"
|
||||||
"To: victim@example.net\r\n"
|
"To: victim@example.net\r\n"
|
||||||
"Subject: Synthetic fixture\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"
|
"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"
|
"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"
|
" 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"
|
"Received: from localhost ([127.0.0.1]) by mail.example.test\r\n"
|
||||||
|
|
@ -26,6 +28,7 @@ static void test_eml_analyzer_headers(void)
|
||||||
EmlAnalysis *analysis = NULL;
|
EmlAnalysis *analysis = NULL;
|
||||||
const GPtrArray *received = NULL, *emails = NULL, *ips = NULL;
|
const GPtrArray *received = NULL, *emails = NULL, *ips = NULL;
|
||||||
const GPtrArray *sender_ips = NULL, *destination_ips = NULL;
|
const GPtrArray *sender_ips = NULL, *destination_ips = NULL;
|
||||||
|
const GPtrArray *domains = NULL, *observations = NULL;
|
||||||
GError *error = NULL;
|
GError *error = NULL;
|
||||||
directory = g_dir_make_tmp("labfy-eml-test-XXXXXX", &error);
|
directory = g_dir_make_tmp("labfy-eml-test-XXXXXX", &error);
|
||||||
assert(directory != NULL && error == NULL);
|
assert(directory != NULL && error == NULL);
|
||||||
|
|
@ -35,6 +38,8 @@ static void test_eml_analyzer_headers(void)
|
||||||
assert(analysis != NULL && error == NULL);
|
assert(analysis != NULL && error == NULL);
|
||||||
assert(strcmp(eml_analysis_get_first_header(analysis, "from"),
|
assert(strcmp(eml_analysis_get_first_header(analysis, "from"),
|
||||||
"Example Sender <sender@example.test>") == 0);
|
"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");
|
received = eml_analysis_get_header_values(analysis, "Received");
|
||||||
assert(received != NULL && received->len == 2);
|
assert(received != NULL && received->len == 2);
|
||||||
assert(strstr(g_ptr_array_index((GPtrArray *) received, 0), " by mx.example.net") != NULL);
|
assert(strstr(g_ptr_array_index((GPtrArray *) received, 0), " by mx.example.net") != NULL);
|
||||||
|
|
@ -53,12 +58,56 @@ static void test_eml_analyzer_headers(void)
|
||||||
"192.0.2.10") == 0);
|
"192.0.2.10") == 0);
|
||||||
assert(strcmp(g_ptr_array_index((GPtrArray *) destination_ips, 0),
|
assert(strcmp(g_ptr_array_index((GPtrArray *) destination_ips, 0),
|
||||||
"198.51.100.20") == 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);
|
eml_analysis_free(analysis);
|
||||||
assert(g_remove(path) == 0); assert(g_rmdir(directory) == 0);
|
assert(g_remove(path) == 0); assert(g_rmdir(directory) == 0);
|
||||||
g_free(path); g_free(directory);
|
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)
|
int main(void)
|
||||||
{
|
{
|
||||||
test_eml_analyzer_headers();
|
test_eml_analyzer_headers();
|
||||||
|
test_eml_analyzer_manual_fixture_regression();
|
||||||
puts("EmlAnalyzer : tous les tests sont valides."); return 0;
|
puts("EmlAnalyzer : tous les tests sont valides."); return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
202
tests/test_eml_integration.c
Normal file
202
tests/test_eml_integration.c
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
#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;
|
||||||
|
}
|
||||||
736
tests/test_eml_mime_extractor.c
Normal file
736
tests/test_eml_mime_extractor.c
Normal file
|
|
@ -0,0 +1,736 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* @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();
|
||||||
|
}
|
||||||
|
|
@ -68,9 +68,9 @@ static void test_eml_pipeline_basic(void)
|
||||||
EmlAttachment *att = g_ptr_array_index(result->mime_result->attachments, 0);
|
EmlAttachment *att = g_ptr_array_index(result->mime_result->attachments, 0);
|
||||||
g_assert_cmpstr(att->sanitized_filename, ==, "___rib_suspect.txt");
|
g_assert_cmpstr(att->sanitized_filename, ==, "___rib_suspect.txt");
|
||||||
att = g_ptr_array_index(result->mime_result->attachments, 1);
|
att = g_ptr_array_index(result->mime_result->attachments, 1);
|
||||||
g_assert_cmpstr(att->sanitized_filename, ==, "___rib_suspect.txt");
|
g_assert_cmpstr(att->sanitized_filename, ==, "___rib_suspect-2.txt");
|
||||||
g_assert_true(g_str_has_suffix(att->extracted_path,
|
g_assert_true(g_str_has_suffix(att->extracted_path,
|
||||||
"/1____rib_suspect.txt"));
|
"/___rib_suspect-2.txt"));
|
||||||
g_assert_cmpuint(att->decoded_size, >, 0U);
|
g_assert_cmpuint(att->decoded_size, >, 0U);
|
||||||
|
|
||||||
/* Vérification de la détection de la proposition bancaire dans la pièce jointe */
|
/* Vérification de la détection de la proposition bancaire dans la pièce jointe */
|
||||||
|
|
@ -86,9 +86,249 @@ static void test_eml_pipeline_basic(void)
|
||||||
g_free(tmp_dir);
|
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)
|
int main(int argc, char **argv)
|
||||||
{
|
{
|
||||||
g_test_init(&argc, &argv, NULL);
|
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/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();
|
return g_test_run();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -523,6 +523,7 @@ static void test_evidence_entity_dao_link_valid(void)
|
||||||
|
|
||||||
GError *error =
|
GError *error =
|
||||||
NULL;
|
NULL;
|
||||||
|
char *observation_identifier = NULL;
|
||||||
|
|
||||||
test_evidence_entity_dao_insert_evidence(
|
test_evidence_entity_dao_insert_evidence(
|
||||||
&fixture,
|
&fixture,
|
||||||
|
|
@ -565,6 +566,30 @@ static void test_evidence_entity_dao_link_valid(void)
|
||||||
|
|
||||||
assert(error == NULL);
|
assert(error == NULL);
|
||||||
assert(association_exists);
|
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(
|
test_evidence_entity_dao_fixture_clear(
|
||||||
&fixture
|
&fixture
|
||||||
|
|
@ -572,7 +597,7 @@ static void test_evidence_entity_dao_link_valid(void)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Vérifie le refus d'une association dupliquée.
|
* @brief Vérifie l'idempotence d'une association manuelle.
|
||||||
*/
|
*/
|
||||||
static void test_evidence_entity_dao_link_duplicate(void)
|
static void test_evidence_entity_dao_link_duplicate(void)
|
||||||
{
|
{
|
||||||
|
|
@ -612,7 +637,7 @@ static void test_evidence_entity_dao_link_duplicate(void)
|
||||||
assert(error == NULL);
|
assert(error == NULL);
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
!evidence_entity_dao_link(
|
evidence_entity_dao_link(
|
||||||
fixture.evidence_entity_dao,
|
fixture.evidence_entity_dao,
|
||||||
evidence_identifier,
|
evidence_identifier,
|
||||||
entity_identifier,
|
entity_identifier,
|
||||||
|
|
@ -620,10 +645,7 @@ static void test_evidence_entity_dao_link_duplicate(void)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
test_evidence_entity_dao_assert_error(
|
assert(error == NULL);
|
||||||
error,
|
|
||||||
EVIDENCE_ENTITY_DAO_ERROR_CONSTRAINT
|
|
||||||
);
|
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
test_evidence_entity_dao_count_rows(
|
test_evidence_entity_dao_count_rows(
|
||||||
|
|
@ -631,10 +653,6 @@ static void test_evidence_entity_dao_link_duplicate(void)
|
||||||
) == 1
|
) == 1
|
||||||
);
|
);
|
||||||
|
|
||||||
g_clear_error(
|
|
||||||
&error
|
|
||||||
);
|
|
||||||
|
|
||||||
test_evidence_entity_dao_fixture_clear(
|
test_evidence_entity_dao_fixture_clear(
|
||||||
&fixture
|
&fixture
|
||||||
);
|
);
|
||||||
|
|
|
||||||
130
tests/test_exiftool_analysis.c
Normal file
130
tests/test_exiftool_analysis.c
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* @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();
|
||||||
|
}
|
||||||
111
tests/test_ocr_analysis.c
Normal file
111
tests/test_ocr_analysis.c
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* @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();
|
||||||
|
}
|
||||||
156
tests/test_pdf_analysis.c
Normal file
156
tests/test_pdf_analysis.c
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
/******************************************************************************
|
||||||
|
* @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();
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue