labfy-investigation/src/core/extraction_drop_service.c
2026-07-23 13:58:23 +02:00

361 lines
12 KiB
C

/******************************************************************************
* @file extraction_drop_service.c
* @brief Validation et intégration d'une extraction déposée sur le graphe.
******************************************************************************/
#include "core/extraction_drop_service.h"
#include "core/file_hash.h"
#include "dao/entity_dao.h"
#include "dao/evidence_dao.h"
#include "dao/evidence_entity_dao.h"
#include "database/transaction.h"
#include "models/entity_record.h"
#include "models/evidence_record.h"
#include <string.h>
struct ExtractionDropInfo
{
char *file_path;
char *relative_path;
char *file_name;
char *source;
char *content;
};
GQuark extraction_drop_service_error_quark(void)
{
return g_quark_from_static_string("extraction-drop-service-error");
}
static void extraction_drop_service_set_error(
GError **error,
ExtractionDropServiceError code,
const char *message)
{
if (error != NULL && *error == NULL)
{
g_set_error_literal(error, EXTRACTION_DROP_SERVICE_ERROR, code, message);
}
}
static char *extraction_drop_service_timestamp(void)
{
GDateTime *now = g_date_time_new_now_utc();
char *timestamp = now != NULL
? g_date_time_format(now, "%Y-%m-%dT%H:%M:%SZ") : NULL;
g_clear_pointer(&now, g_date_time_unref);
return timestamp;
}
static char *extraction_drop_service_source_from_content(const char *content)
{
const char *line_end = NULL;
if (content == NULL || content[0] == '\0')
{
return g_strdup("Extraction locale");
}
line_end = strchr(content, '\n');
if (line_end != NULL && line_end > content)
{
return g_strndup(content, (gsize) (line_end - content));
}
return g_strdup(content);
}
ExtractionDropInfo *extraction_drop_service_prepare(
const char *investigation_root,
const char *file_path,
GError **error)
{
ExtractionDropInfo *info = NULL;
char *canonical_root = NULL;
char *extractions_root = NULL;
char *canonical_extractions_root = NULL;
char *canonical_file = NULL;
char *content = NULL;
gsize content_length = 0U;
g_return_val_if_fail(error == NULL || *error == NULL, NULL);
if (investigation_root == NULL || investigation_root[0] == '\0' ||
file_path == NULL || file_path[0] == '\0')
{
extraction_drop_service_set_error(error,
EXTRACTION_DROP_SERVICE_ERROR_INVALID_ARGUMENT,
"Le chemin de l'extraction est invalide.");
return NULL;
}
canonical_root = g_canonicalize_filename(investigation_root, NULL);
extractions_root = g_build_filename(canonical_root,
"02_Preuves_Traitees", "Extractions", NULL);
canonical_extractions_root = g_canonicalize_filename(extractions_root, NULL);
canonical_file = g_canonicalize_filename(file_path, NULL);
if (canonical_root == NULL || canonical_extractions_root == NULL ||
canonical_file == NULL)
{
extraction_drop_service_set_error(error,
EXTRACTION_DROP_SERVICE_ERROR_INVALID_ARGUMENT,
"Impossible de normaliser le chemin de l'extraction.");
goto cleanup;
}
if (!g_str_has_prefix(canonical_file, canonical_extractions_root) ||
canonical_file[strlen(canonical_extractions_root)] != G_DIR_SEPARATOR)
{
extraction_drop_service_set_error(error,
EXTRACTION_DROP_SERVICE_ERROR_OUTSIDE_INVESTIGATION,
"Le fichier ne se trouve pas dans les extractions de l'enquête.");
goto cleanup;
}
if (!g_file_test(canonical_file, G_FILE_TEST_IS_REGULAR) ||
g_file_test(canonical_file, G_FILE_TEST_IS_SYMLINK) ||
!g_str_has_suffix(canonical_file, ".txt"))
{
extraction_drop_service_set_error(error,
EXTRACTION_DROP_SERVICE_ERROR_INVALID_FILE,
"Seuls les fichiers texte réguliers d'extraction sont acceptés.");
goto cleanup;
}
if (!g_file_get_contents(canonical_file, &content, &content_length, error))
{
if (error != NULL && *error != NULL)
{
(*error)->domain = EXTRACTION_DROP_SERVICE_ERROR;
(*error)->code = EXTRACTION_DROP_SERVICE_ERROR_READ;
}
goto cleanup;
}
if (!g_utf8_validate(content, (gssize) content_length, NULL))
{
extraction_drop_service_set_error(error,
EXTRACTION_DROP_SERVICE_ERROR_INVALID_FILE,
"L'extraction texte n'est pas encodée en UTF-8.");
goto cleanup;
}
info = g_new0(ExtractionDropInfo, 1);
info->file_path = g_strdup(canonical_file);
info->relative_path = g_strdup(canonical_file + strlen(canonical_root) + 1U);
info->file_name = g_path_get_basename(canonical_file);
info->content = g_steal_pointer(&content);
info->source = extraction_drop_service_source_from_content(info->content);
if (info->file_path == NULL || info->relative_path == NULL ||
info->file_name == NULL || info->content == NULL || info->source == NULL)
{
extraction_drop_info_free(info);
info = NULL;
extraction_drop_service_set_error(error,
EXTRACTION_DROP_SERVICE_ERROR_INVALID_ARGUMENT,
"Impossible d'allouer les informations de l'extraction.");
}
cleanup:
g_free(content);
g_free(canonical_file);
g_free(canonical_extractions_root);
g_free(extractions_root);
g_free(canonical_root);
return info;
}
void extraction_drop_info_free(ExtractionDropInfo *info)
{
if (info == NULL) return;
g_free(info->file_path);
g_free(info->relative_path);
g_free(info->file_name);
g_free(info->source);
g_free(info->content);
g_free(info);
}
#define EXTRACTION_DROP_GETTER(name, field) \
const char *name(const ExtractionDropInfo *info) \
{ return info != NULL ? info->field : NULL; }
EXTRACTION_DROP_GETTER(extraction_drop_info_get_file_path, file_path)
EXTRACTION_DROP_GETTER(extraction_drop_info_get_relative_path, relative_path)
EXTRACTION_DROP_GETTER(extraction_drop_info_get_file_name, file_name)
EXTRACTION_DROP_GETTER(extraction_drop_info_get_source, source)
EXTRACTION_DROP_GETTER(extraction_drop_info_get_content, content)
static char *extraction_drop_service_ensure_evidence(
Database *database,
const ExtractionDropInfo *info,
GError **error)
{
EvidenceDao *dao = NULL;
EvidenceRecord *record = NULL;
GPtrArray *records = NULL;
char *identifier = NULL;
char *sha256 = NULL;
char *timestamp = NULL;
guint64 size_bytes = 0U;
dao = evidence_dao_new(database, error);
if (dao == NULL) goto cleanup;
records = evidence_dao_list_all(dao, error);
if (records == NULL) goto cleanup;
for (guint index = 0U; index < records->len; index++)
{
const EvidenceRecord *candidate = g_ptr_array_index(records, index);
if (g_strcmp0(evidence_record_get_relative_path(candidate),
info->relative_path) == 0)
{
identifier = g_strdup(evidence_record_get_identifier(candidate));
goto cleanup;
}
}
if (!file_hash_compute_sha256(info->file_path, NULL, &sha256,
&size_bytes, error)) goto cleanup;
identifier = g_uuid_string_random();
timestamp = extraction_drop_service_timestamp();
record = evidence_record_new(identifier, info->file_name, info->file_name,
info->relative_path, "text", size_bytes, sha256, timestamp, NULL,
info->source, "Extraction intégrée depuis le graphe.",
EVIDENCE_INTEGRITY_STATUS_VALID, error);
if (record == NULL || !evidence_dao_insert(dao, record, error))
{
g_clear_pointer(&identifier, g_free);
}
cleanup:
evidence_record_free(record);
g_clear_pointer(&records, g_ptr_array_unref);
evidence_dao_free(dao);
g_free(timestamp);
g_free(sha256);
return identifier;
}
static gboolean extraction_drop_service_attach_in_transaction(
Database *database,
const ExtractionDropInfo *info,
const char *entity_identifier,
GError **error)
{
EvidenceEntityDao *link_dao = NULL;
char *evidence_identifier = NULL;
gboolean exists = FALSE;
gboolean success = FALSE;
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
if (database == NULL || info == NULL || entity_identifier == NULL ||
!g_uuid_string_is_valid(entity_identifier))
{
extraction_drop_service_set_error(error,
EXTRACTION_DROP_SERVICE_ERROR_INVALID_ARGUMENT,
"L'entité de destination est invalide.");
return FALSE;
}
evidence_identifier = extraction_drop_service_ensure_evidence(database,
info, error);
if (evidence_identifier == NULL) goto cleanup;
link_dao = evidence_entity_dao_new(database, error);
if (link_dao == NULL ||
!evidence_entity_dao_exists(link_dao, evidence_identifier,
entity_identifier, &exists, error) ||
(!exists && !evidence_entity_dao_link(link_dao, evidence_identifier,
entity_identifier, error))) goto cleanup;
success = TRUE;
cleanup:
evidence_entity_dao_free(link_dao);
g_free(evidence_identifier);
return success;
}
gboolean extraction_drop_service_attach(
Database *database,
const ExtractionDropInfo *info,
const char *entity_identifier,
GError **error)
{
gboolean success = FALSE;
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
if (database == NULL || info == NULL || entity_identifier == NULL ||
!g_uuid_string_is_valid(entity_identifier))
{
extraction_drop_service_set_error(error,
EXTRACTION_DROP_SERVICE_ERROR_INVALID_ARGUMENT,
"L'entité de destination est invalide.");
return FALSE;
}
if (!database_transaction_begin(database)) goto database_failure;
if (!extraction_drop_service_attach_in_transaction(database, info,
entity_identifier, error))
{
database_transaction_rollback(database);
return FALSE;
}
if (!database_transaction_commit(database)) goto database_failure;
success = TRUE;
return success;
database_failure:
extraction_drop_service_set_error(error,
EXTRACTION_DROP_SERVICE_ERROR_DATABASE,
"Impossible d'enregistrer l'association de l'extraction.");
return FALSE;
}
gboolean extraction_drop_service_create_entity(
Database *database,
const ExtractionDropInfo *info,
char **out_entity_identifier,
GError **error)
{
EntityDao *entity_dao = NULL;
EntityRecord *entity = NULL;
char *identifier = NULL;
char *timestamp = NULL;
gboolean success = FALSE;
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
if (out_entity_identifier != NULL) *out_entity_identifier = NULL;
if (database == NULL || info == NULL)
{
extraction_drop_service_set_error(error,
EXTRACTION_DROP_SERVICE_ERROR_INVALID_ARGUMENT,
"Les paramètres de création d'entité sont invalides.");
return FALSE;
}
identifier = g_uuid_string_random();
timestamp = extraction_drop_service_timestamp();
entity = entity_record_new(identifier, "other", info->file_name,
info->file_name, info->content, 50, timestamp, timestamp,
ENTITY_STATUS_ACTIVE, error);
if (!database_transaction_begin(database))
{
extraction_drop_service_set_error(error,
EXTRACTION_DROP_SERVICE_ERROR_DATABASE,
"Impossible de démarrer la création de l'entité.");
goto cleanup;
}
entity_dao = entity_dao_new(database, error);
if (entity == NULL || entity_dao == NULL ||
!entity_dao_insert(entity_dao, entity, error) ||
!extraction_drop_service_attach_in_transaction(database, info,
identifier, error))
{
database_transaction_rollback(database);
goto cleanup;
}
if (!database_transaction_commit(database))
{
extraction_drop_service_set_error(error,
EXTRACTION_DROP_SERVICE_ERROR_DATABASE,
"Impossible de valider la création de l'entité.");
goto cleanup;
}
if (out_entity_identifier != NULL)
*out_entity_identifier = g_strdup(identifier);
success = TRUE;
cleanup:
entity_dao_free(entity_dao);
entity_record_free(entity);
g_free(timestamp);
g_free(identifier);
return success;
}