diff --git a/Makefile b/Makefile index a918eac..0c073bd 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,6 @@ TEST_TRANSACTION = tests/test_transaction TEST_ERROR = tests/test_error TEST_INVESTIGATION_RECORD = tests/test_investigation_record TEST_EVIDENCE_RECORD := tests/test_evidence_record -TEST_EVIDENCE_RECORD := tests/test_evidence_record TEST_INVESTIGATION_DAO := tests/test_investigation_dao TEST_EVIDENCE_DAO := tests/test_evidence_dao TEST_INVESTIGATION_SESSION := tests/test_investigation_session @@ -53,6 +52,7 @@ TEST_TOOL_TASK := tests/test_tool_task TEST_TOOL_CATALOG := tests/test_tool_catalog TEST_TOOL_INITIALIZER := tests/test_tool_initializer TEST_FILE_HASH := tests/test_file_hash +TEST_EVIDENCE_COPY := tests/test_evidence_copy all: $(TARGET) @@ -220,6 +220,14 @@ $(TEST_FILE_HASH): \ -DFILE_HASH_ENABLE_TEST_HOOKS \ $^ -o $@ $(TEST_LDFLAGS) +$(TEST_EVIDENCE_COPY): \ + tests/test_evidence_copy.c \ + src/core/evidence_copy.c \ + src/core/file_hash.c + $(CC) $(TEST_CFLAGS) \ + -DEVIDENCE_COPY_ENABLE_TEST_HOOKS \ + $^ -o $@ $(TEST_LDFLAGS) + test: \ $(TEST_NODE) \ $(TEST_TREE_MODEL) \ @@ -241,7 +249,8 @@ test: \ $(TEST_TOOL_TASK) \ $(TEST_TOOL_CATALOG) \ $(TEST_TOOL_INITIALIZER) \ - $(TEST_FILE_HASH) + $(TEST_FILE_HASH) \ + $(TEST_EVIDENCE_COPY) @echo "Exécution des tests..." @./$(TEST_NODE) @./$(TEST_TREE_MODEL) @@ -264,6 +273,7 @@ test: \ @$(TEST_TOOL_CATALOG) @$(TEST_TOOL_INITIALIZER) @$(TEST_FILE_HASH) + @$(TEST_EVIDENCE_COPY) @echo "Tous les tests sont valides." %.o: %.c @@ -294,7 +304,8 @@ clean: $(TEST_TOOL_TASK) \ $(TEST_TOOL_CATALOG) \ $(TEST_TOOL_INITIALIZER) \ - $(TEST_FILE_HASH) + $(TEST_FILE_HASH) \ + $(TEST_EVIDENCE_COPY) .PHONY: clean run test diff --git a/include/core/evidence_copy.h b/include/core/evidence_copy.h new file mode 100644 index 0000000..c8a2eb7 --- /dev/null +++ b/include/core/evidence_copy.h @@ -0,0 +1,199 @@ +/****************************************************************************** + * @file evidence_copy.h + * @brief Copie sûre et vérifiée des fichiers de preuve. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_EVIDENCE_COPY_H +#define LABFY_INVESTIGATION_EVIDENCE_COPY_H + +#include +#include + +/** + * @brief Catégories d’erreurs du module EvidenceCopy. + */ +typedef enum +{ + EVIDENCE_COPY_ERROR_INVALID_ARGUMENT, + EVIDENCE_COPY_ERROR_SOURCE_NOT_FOUND, + EVIDENCE_COPY_ERROR_SOURCE_NOT_REGULAR, + EVIDENCE_COPY_ERROR_DESTINATION_INVALID, + EVIDENCE_COPY_ERROR_DESTINATION_EXISTS, + EVIDENCE_COPY_ERROR_OPEN_SOURCE, + EVIDENCE_COPY_ERROR_CREATE_DESTINATION, + EVIDENCE_COPY_ERROR_READ, + EVIDENCE_COPY_ERROR_WRITE, + EVIDENCE_COPY_ERROR_SYNC, + EVIDENCE_COPY_ERROR_HASH, + EVIDENCE_COPY_ERROR_VERIFY, + EVIDENCE_COPY_ERROR_CANCELLED, + EVIDENCE_COPY_ERROR_MEMORY, + EVIDENCE_COPY_ERROR_CLEANUP +} EvidenceCopyError; + +/** + * @brief Domaine d’erreurs du module EvidenceCopy. + */ +#define EVIDENCE_COPY_ERROR \ + evidence_copy_error_quark() + +/** + * @brief Retourne le domaine d’erreurs du module EvidenceCopy. + */ +GQuark evidence_copy_error_quark(void); + +/** + * @brief Résultat opaque d’une copie de preuve validée. + */ +typedef struct EvidenceCopyResult EvidenceCopyResult; + +/** + * @brief Copie et vérifie un fichier de preuve. + * + * La fonction : + * + * - refuse les sources non régulières ; + * - refuse un dossier destination symbolique ; + * - crée exclusivement un nouveau fichier ; + * - copie le contenu progressivement ; + * - synchronise les données écrites ; + * - compare la taille et le SHA-256 de la source et de la destination ; + * - supprime la destination en cas d’échec après sa création. + * + * Le fichier source n’est jamais modifié. + * + * Le nom destination doit représenter uniquement un nom de fichier : + * il ne doit contenir aucun séparateur de chemin. + * + * @param source_path Chemin du fichier source. + * @param destination_directory Dossier recevant la copie. + * @param destination_name Nom du fichier destination. + * @param cancellable Objet d’annulation facultatif. + * @param error Adresse recevant une éventuelle erreur. + * + * @return Nouveau résultat possédé par l’appelant, ou NULL. + */ +EvidenceCopyResult *evidence_copy_file( + const char *source_path, + const char *destination_directory, + const char *destination_name, + GCancellable *cancellable, + GError **error +); + +/** + * @brief Libère un résultat de copie. + * + * La copie validée présente sur le disque n’est pas supprimée. + * Cette fonction accepte NULL. + * + * @param result Résultat à libérer. + */ +void evidence_copy_result_free( + EvidenceCopyResult *result +); + +/** + * @brief Retourne le chemin complet de la copie validée. + * + * Le pointeur appartient au résultat. + * + * @return Chemin destination, ou NULL. + */ +const char *evidence_copy_result_get_destination_path( + const EvidenceCopyResult *result +); + +/** + * @brief Retourne le nom du fichier destination. + * + * Le pointeur appartient au résultat. + * + * @return Nom destination, ou NULL. + */ +const char *evidence_copy_result_get_destination_name( + const EvidenceCopyResult *result +); + +/** + * @brief Retourne le SHA-256 validé de la copie. + * + * Le pointeur appartient au résultat. + * + * @return SHA-256 sur 64 caractères, ou NULL. + */ +const char *evidence_copy_result_get_sha256( + const EvidenceCopyResult *result +); + +/** + * @brief Retourne la taille validée de la copie. + * + * @return Taille en octets, ou zéro si result vaut NULL. + */ +guint64 evidence_copy_result_get_size_bytes( + const EvidenceCopyResult *result +); + +#ifdef EVIDENCE_COPY_ENABLE_TEST_HOOKS + +/** + * @brief Crochet appelé après chaque bloc copié. + */ +typedef void (*EvidenceCopyTestBlockHook)( + guint64 copied_size_bytes, + gpointer user_data +); + +void evidence_copy_test_set_block_hook( + EvidenceCopyTestBlockHook block_hook, + gpointer user_data +); + +/** + * @brief Crochet appelé avant chaque tentative d’écriture. + */ +typedef gboolean (*EvidenceCopyTestBeforeWriteHook)( + guint64 written_size_bytes, + gpointer user_data +); + +void evidence_copy_test_set_before_write_hook( + EvidenceCopyTestBeforeWriteHook before_write_hook, + gpointer user_data +); + +/** + * @brief Crochet appelé avant la vérification cryptographique finale. + */ +typedef void (*EvidenceCopyTestBeforeVerifyHook)( + const char *destination_path, + gpointer user_data +); + +void evidence_copy_test_set_before_verify_hook( + EvidenceCopyTestBeforeVerifyHook before_verify_hook, + gpointer user_data +); + +/** + * @brief Crochet appelé avant la suppression d’une copie invalide. + * + * Retourner TRUE simule un échec de suppression. + */ +typedef gboolean (*EvidenceCopyTestCleanupHook)( + const char *destination_path, + gpointer user_data +); + +/** + * @brief Configure le crochet de nettoyage réservé aux tests. + */ +void evidence_copy_test_set_cleanup_hook( + EvidenceCopyTestCleanupHook cleanup_hook, + gpointer user_data +); + +#endif + +#endif diff --git a/labfy-investigation b/labfy-investigation index f806d19..5fe153d 100755 Binary files a/labfy-investigation and b/labfy-investigation differ diff --git a/src/core/evidence_copy.c b/src/core/evidence_copy.c new file mode 100644 index 0000000..a629604 --- /dev/null +++ b/src/core/evidence_copy.c @@ -0,0 +1,1572 @@ +/****************************************************************************** + * @file evidence_copy.c + * @brief Copie sûre et vérifiée des fichiers de preuve. + ******************************************************************************/ + +#define _POSIX_C_SOURCE 200809L + +#include "core/evidence_copy.h" +#include "core/file_hash.h" + +#include +#include +#include +#include +#include + +/** + * @brief Taille du tampon utilisé pour la copie. + */ +#define EVIDENCE_COPY_BLOCK_SIZE (64U * 1024U) + +#ifdef EVIDENCE_COPY_ENABLE_TEST_HOOKS + +static EvidenceCopyTestBlockHook + evidence_copy_test_block_hook = NULL; + +static gpointer + evidence_copy_test_block_hook_data = NULL; + +void evidence_copy_test_set_block_hook( + EvidenceCopyTestBlockHook block_hook, + gpointer user_data +) +{ + evidence_copy_test_block_hook = + block_hook; + + evidence_copy_test_block_hook_data = + user_data; +} + +static void evidence_copy_test_notify_block( + guint64 copied_size_bytes +) +{ + if (evidence_copy_test_block_hook == NULL) + { + return; + } + + evidence_copy_test_block_hook( + copied_size_bytes, + evidence_copy_test_block_hook_data + ); +} + +static EvidenceCopyTestBeforeWriteHook + evidence_copy_test_before_write_hook = NULL; + +static gpointer + evidence_copy_test_before_write_hook_data = NULL; + +void evidence_copy_test_set_before_write_hook( + EvidenceCopyTestBeforeWriteHook before_write_hook, + gpointer user_data +) +{ + evidence_copy_test_before_write_hook = + before_write_hook; + + evidence_copy_test_before_write_hook_data = + user_data; +} + +static gboolean evidence_copy_test_should_fail_write( + guint64 written_size_bytes +) +{ + if (evidence_copy_test_before_write_hook == NULL) + { + return FALSE; + } + + return evidence_copy_test_before_write_hook( + written_size_bytes, + evidence_copy_test_before_write_hook_data + ); +} + +/** + * @brief Crochet appelé avant la vérification cryptographique finale. + */ +typedef void (*EvidenceCopyTestBeforeVerifyHook)( + const char *destination_path, + gpointer user_data +); + +/** + * @brief Configure le crochet de vérification réservé aux tests. + */ +void evidence_copy_test_set_before_verify_hook( + EvidenceCopyTestBeforeVerifyHook before_verify_hook, + gpointer user_data +); + +static EvidenceCopyTestBeforeVerifyHook + evidence_copy_test_before_verify_hook = NULL; + +static gpointer + evidence_copy_test_before_verify_hook_data = NULL; + +void evidence_copy_test_set_before_verify_hook( + EvidenceCopyTestBeforeVerifyHook before_verify_hook, + gpointer user_data +) +{ + evidence_copy_test_before_verify_hook = + before_verify_hook; + + evidence_copy_test_before_verify_hook_data = + user_data; +} + +static void evidence_copy_test_notify_before_verify( + const char *destination_path +) +{ + if (evidence_copy_test_before_verify_hook == NULL) + { + return; + } + + evidence_copy_test_before_verify_hook( + destination_path, + evidence_copy_test_before_verify_hook_data + ); +} + +static EvidenceCopyTestCleanupHook + evidence_copy_test_cleanup_hook = NULL; + +static gpointer + evidence_copy_test_cleanup_hook_data = NULL; + +void evidence_copy_test_set_cleanup_hook( + EvidenceCopyTestCleanupHook cleanup_hook, + gpointer user_data +) +{ + evidence_copy_test_cleanup_hook = + cleanup_hook; + + evidence_copy_test_cleanup_hook_data = + user_data; +} + +static gboolean evidence_copy_test_should_fail_cleanup( + const char *destination_path +) +{ + if (evidence_copy_test_cleanup_hook == NULL) + { + return FALSE; + } + + return evidence_copy_test_cleanup_hook( + destination_path, + evidence_copy_test_cleanup_hook_data + ); +} + +#else + +static void evidence_copy_test_notify_block( + guint64 copied_size_bytes +) +{ + (void) copied_size_bytes; +} + +static gboolean evidence_copy_test_should_fail_write( + guint64 written_size_bytes +) +{ + (void) written_size_bytes; + + return FALSE; +} + +static void evidence_copy_test_notify_before_verify( + const char *destination_path +) +{ + (void) destination_path; +} + +static gboolean evidence_copy_test_should_fail_cleanup( + const char *destination_path +) +{ + (void) destination_path; + + return FALSE; +} + +#endif + +/** + * @brief Représentation privée du résultat d’une copie validée. + */ +struct EvidenceCopyResult +{ + char *destination_path; + char *destination_name; + char *sha256; + + guint64 size_bytes; +}; + +/** + * @brief Enregistre une erreur littérale EvidenceCopy. + */ +static void evidence_copy_set_error_literal( + GError **error, + EvidenceCopyError error_code, + const char *error_message +) +{ + if (error == NULL) + { + return; + } + + g_set_error_literal( + error, + EVIDENCE_COPY_ERROR, + error_code, + error_message + ); +} + +/** + * @brief Enregistre une erreur système EvidenceCopy. + */ +static void evidence_copy_set_system_error( + GError **error, + EvidenceCopyError error_code, + const char *context, + const char *file_path, + int system_error +) +{ + if (error == NULL) + { + return; + } + + g_set_error( + error, + EVIDENCE_COPY_ERROR, + error_code, + "%s '%s' : %s", + context, + file_path, + g_strerror(system_error) + ); +} + +/** + * @brief Vérifie si une annulation a été demandée. + */ +static gboolean evidence_copy_check_cancelled( + GCancellable *cancellable, + GError **error +) +{ + if (cancellable == NULL || + !g_cancellable_is_cancelled(cancellable)) + { + return FALSE; + } + + evidence_copy_set_error_literal( + error, + EVIDENCE_COPY_ERROR_CANCELLED, + "La copie de la preuve a été annulée." + ); + + return TRUE; +} + +/** + * @brief Vérifie qu’un nom représente uniquement un fichier. + */ +static gboolean evidence_copy_destination_name_is_valid( + const char *destination_name +) +{ + if (destination_name == NULL || + destination_name[0] == '\0') + { + return FALSE; + } + + if (strcmp( + destination_name, + "." + ) == 0 || + strcmp( + destination_name, + ".." + ) == 0) + { + return FALSE; + } + + if (strchr( + destination_name, + '/' + ) != NULL || + strchr( + destination_name, + '\\' + ) != NULL) + { + return FALSE; + } + + return TRUE; +} + +/** + * @brief Traduit une erreur FileHash en erreur EvidenceCopy. + */ +static EvidenceCopyError evidence_copy_map_hash_error( + const GError *hash_error +) +{ + if (hash_error == NULL) + { + return EVIDENCE_COPY_ERROR_HASH; + } + + if (g_error_matches( + hash_error, + FILE_HASH_ERROR, + FILE_HASH_ERROR_NOT_FOUND + )) + { + return EVIDENCE_COPY_ERROR_SOURCE_NOT_FOUND; + } + + if (g_error_matches( + hash_error, + FILE_HASH_ERROR, + FILE_HASH_ERROR_NOT_REGULAR + )) + { + return EVIDENCE_COPY_ERROR_SOURCE_NOT_REGULAR; + } + + if (g_error_matches( + hash_error, + FILE_HASH_ERROR, + FILE_HASH_ERROR_CANCELLED + )) + { + return EVIDENCE_COPY_ERROR_CANCELLED; + } + + return EVIDENCE_COPY_ERROR_HASH; +} + +/** + * @brief Convertit et conserve le contexte d’une erreur FileHash. + */ +static void evidence_copy_set_hash_error( + GError **error, + const GError *hash_error +) +{ + EvidenceCopyError error_code = + evidence_copy_map_hash_error( + hash_error + ); + + if (error == NULL) + { + return; + } + + g_set_error( + error, + EVIDENCE_COPY_ERROR, + error_code, + "Impossible d’analyser le fichier source : %s", + hash_error != NULL + ? hash_error->message + : "erreur SHA-256 inconnue" + ); +} + +GQuark evidence_copy_error_quark(void) +{ + return g_quark_from_static_string( + "evidence-copy-error-quark" + ); +} + +/** + * @brief Ouvre et valide le dossier de destination. + * + * Le dernier composant du chemin ne peut pas être un lien symbolique. + * + * @return Descripteur du dossier, ou -1. + */ +static int evidence_copy_open_destination_directory( + const char *destination_directory, + GError **error +) +{ + struct stat directory_status; + + int directory_descriptor = -1; + int saved_errno = 0; + + directory_descriptor = open( + destination_directory, + O_RDONLY | + O_DIRECTORY | + O_CLOEXEC | + O_NOFOLLOW + ); + + if (directory_descriptor < 0) + { + saved_errno = errno; + + evidence_copy_set_system_error( + error, + EVIDENCE_COPY_ERROR_DESTINATION_INVALID, + "Impossible d’ouvrir le dossier de destination", + destination_directory, + saved_errno + ); + + return -1; + } + + if (fstat( + directory_descriptor, + &directory_status + ) != 0) + { + saved_errno = errno; + + evidence_copy_set_system_error( + error, + EVIDENCE_COPY_ERROR_DESTINATION_INVALID, + "Impossible d’inspecter le dossier de destination", + destination_directory, + saved_errno + ); + + close( + directory_descriptor + ); + + return -1; + } + + if (!S_ISDIR(directory_status.st_mode)) + { + evidence_copy_set_error_literal( + error, + EVIDENCE_COPY_ERROR_DESTINATION_INVALID, + "Le chemin de destination ne désigne pas un dossier." + ); + + close( + directory_descriptor + ); + + return -1; + } + + return directory_descriptor; +} + +/** + * @brief Ouvre et valide le fichier source. + * + * @return Descripteur du fichier source, ou -1. + */ +static int evidence_copy_open_source_file( + const char *source_path, + GError **error +) +{ + struct stat source_status; + + int source_descriptor = -1; + int saved_errno = 0; + + source_descriptor = open( + source_path, + O_RDONLY | + O_CLOEXEC | + O_NOFOLLOW | + O_NONBLOCK + ); + + if (source_descriptor < 0) + { + saved_errno = errno; + + if (saved_errno == ENOENT || + saved_errno == ENOTDIR) + { + evidence_copy_set_system_error( + error, + EVIDENCE_COPY_ERROR_SOURCE_NOT_FOUND, + "Le fichier source est introuvable", + source_path, + saved_errno + ); + } + else if (saved_errno == ELOOP) + { + evidence_copy_set_error_literal( + error, + EVIDENCE_COPY_ERROR_SOURCE_NOT_REGULAR, + "Le fichier source est un lien symbolique." + ); + } + else + { + evidence_copy_set_system_error( + error, + EVIDENCE_COPY_ERROR_OPEN_SOURCE, + "Impossible d’ouvrir le fichier source", + source_path, + saved_errno + ); + } + + return -1; + } + + if (fstat( + source_descriptor, + &source_status + ) != 0) + { + saved_errno = errno; + + evidence_copy_set_system_error( + error, + EVIDENCE_COPY_ERROR_OPEN_SOURCE, + "Impossible d’inspecter le fichier source", + source_path, + saved_errno + ); + + close( + source_descriptor + ); + + return -1; + } + + if (!S_ISREG(source_status.st_mode)) + { + evidence_copy_set_error_literal( + error, + EVIDENCE_COPY_ERROR_SOURCE_NOT_REGULAR, + "Le chemin source ne désigne pas un fichier régulier." + ); + + close( + source_descriptor + ); + + return -1; + } + + return source_descriptor; +} + +/** + * @brief Écrit intégralement un bloc dans le fichier destination. + */ +static gboolean evidence_copy_write_all( + int destination_descriptor, + const guint8 *buffer, + gsize buffer_size, + guint64 copied_size_before_block, + const char *destination_path, + GCancellable *cancellable, + GError **error +) +{ + gsize written_size = 0; + + ssize_t current_write_size = 0; + + int saved_errno = 0; + + while (written_size < buffer_size) + { + if (evidence_copy_check_cancelled( + cancellable, + error + )) + { + return FALSE; + } + + if (evidence_copy_test_should_fail_write( + copied_size_before_block + + (guint64) written_size + )) + { + evidence_copy_set_error_literal( + error, + EVIDENCE_COPY_ERROR_WRITE, + "Une erreur d’écriture simulée est survenue." + ); + + return FALSE; + } + + current_write_size = write( + destination_descriptor, + buffer + written_size, + buffer_size - written_size + ); + + if (current_write_size < 0) + { + saved_errno = errno; + + if (saved_errno == EINTR) + { + continue; + } + + evidence_copy_set_system_error( + error, + EVIDENCE_COPY_ERROR_WRITE, + "Impossible d’écrire dans le fichier destination", + destination_path, + saved_errno + ); + + return FALSE; + } + + if (current_write_size == 0) + { + evidence_copy_set_error_literal( + error, + EVIDENCE_COPY_ERROR_WRITE, + "L’écriture du fichier destination n’a écrit aucun octet." + ); + + return FALSE; + } + + written_size += + (gsize) current_write_size; + } + + return TRUE; +} + +/** + * @brief Copie progressivement le contenu du fichier source. + */ +static gboolean evidence_copy_contents( + int source_descriptor, + int destination_descriptor, + const char *source_path, + const char *destination_path, + GCancellable *cancellable, + guint64 *out_copied_size_bytes, + GError **error +) +{ + guint8 *buffer = NULL; + + guint64 copied_size_bytes = 0; + + ssize_t read_size = 0; + + int saved_errno = 0; + + if (out_copied_size_bytes == NULL) + { + evidence_copy_set_error_literal( + error, + EVIDENCE_COPY_ERROR_INVALID_ARGUMENT, + "La destination de la taille copiée est invalide." + ); + + return FALSE; + } + + *out_copied_size_bytes = 0; + + buffer = g_try_malloc( + EVIDENCE_COPY_BLOCK_SIZE + ); + + if (buffer == NULL) + { + evidence_copy_set_error_literal( + error, + EVIDENCE_COPY_ERROR_MEMORY, + "Impossible d’allouer le tampon de copie." + ); + + return FALSE; + } + + while (TRUE) + { + if (evidence_copy_check_cancelled( + cancellable, + error + )) + { + g_free( + buffer + ); + + return FALSE; + } + + read_size = read( + source_descriptor, + buffer, + EVIDENCE_COPY_BLOCK_SIZE + ); + + if (read_size < 0) + { + saved_errno = errno; + + if (saved_errno == EINTR) + { + continue; + } + + evidence_copy_set_system_error( + error, + EVIDENCE_COPY_ERROR_READ, + "Impossible de lire le fichier source", + source_path, + saved_errno + ); + + g_free( + buffer + ); + + return FALSE; + } + + if (read_size == 0) + { + break; + } + + if (!evidence_copy_write_all( + destination_descriptor, + buffer, + (gsize) read_size, + copied_size_bytes, + destination_path, + cancellable, + error + )) + { + g_free( + buffer + ); + + return FALSE; + } + + copied_size_bytes += + (guint64) read_size; + + evidence_copy_test_notify_block( + copied_size_bytes + ); + + if (evidence_copy_check_cancelled( + cancellable, + error + )) + { + g_free( + buffer + ); + + return FALSE; + } + } + + g_free( + buffer + ); + + *out_copied_size_bytes = + copied_size_bytes; + + return TRUE; +} + +/** + * @brief Crée exclusivement le fichier destination. + * + * Le fichier ne peut pas remplacer un élément existant. + * + * @return Descripteur du fichier créé, ou -1. + */ +static int evidence_copy_create_destination_file( + int destination_directory_descriptor, + const char *destination_name, + const char *destination_path, + GError **error +) +{ + int destination_descriptor = -1; + int saved_errno = 0; + + destination_descriptor = openat( + destination_directory_descriptor, + destination_name, + O_WRONLY | + O_CREAT | + O_EXCL | + O_CLOEXEC | + O_NOFOLLOW, + 0600 + ); + + if (destination_descriptor >= 0) + { + return destination_descriptor; + } + + saved_errno = errno; + + if (saved_errno == EEXIST) + { + evidence_copy_set_system_error( + error, + EVIDENCE_COPY_ERROR_DESTINATION_EXISTS, + "Le fichier destination existe déjà", + destination_path, + saved_errno + ); + } + else + { + evidence_copy_set_system_error( + error, + EVIDENCE_COPY_ERROR_CREATE_DESTINATION, + "Impossible de créer le fichier destination", + destination_path, + saved_errno + ); + } + + return -1; +} + +/** + * @brief Duplique une chaîne en signalant proprement une erreur mémoire. + */ +static char *evidence_copy_duplicate_string( + const char *value, + GError **error +) +{ + char *copy = NULL; + gsize value_size = 0; + + if (value == NULL) + { + evidence_copy_set_error_literal( + error, + EVIDENCE_COPY_ERROR_INVALID_ARGUMENT, + "La chaîne à dupliquer est invalide." + ); + + return NULL; + } + + value_size = + strlen( + value + ); + + copy = g_try_malloc( + value_size + 1U + ); + + if (copy == NULL) + { + evidence_copy_set_error_literal( + error, + EVIDENCE_COPY_ERROR_MEMORY, + "Impossible d’allouer une chaîne du résultat de copie." + ); + + return NULL; + } + + memcpy( + copy, + value, + value_size + 1U + ); + + return copy; +} + +/** + * @brief Signale qu’un fichier incomplet n’a pas pu être supprimé. + */ +static void evidence_copy_report_cleanup_failure( + GError **error, + const char *destination_path, + int system_error +) +{ + char *primary_message = NULL; + + if (error == NULL) + { + g_warning( + "Impossible de supprimer la copie incomplète '%s' : %s", + destination_path, + g_strerror(system_error) + ); + + return; + } + + if (*error != NULL) + { + primary_message = + g_strdup( + (*error)->message + ); + + g_clear_error( + error + ); + } + + g_set_error( + error, + EVIDENCE_COPY_ERROR, + EVIDENCE_COPY_ERROR_CLEANUP, + "Un fichier incomplet peut subsister à l’emplacement '%s' : %s. " + "Cause initiale : %s", + destination_path, + g_strerror(system_error), + primary_message != NULL + ? primary_message + : "erreur inconnue" + ); + + g_free( + primary_message + ); +} + +/** + * @brief Supprime une destination invalide. + * + * @return 0 en cas de succès, sinon -1 avec errno renseigné. + */ +static int evidence_copy_remove_invalid_destination( + int destination_directory_descriptor, + const char *destination_name, + const char *destination_path +) +{ + if (evidence_copy_test_should_fail_cleanup( + destination_path + )) + { + errno = EACCES; + + return -1; + } + + return unlinkat( + destination_directory_descriptor, + destination_name, + 0 + ); +} + +EvidenceCopyResult *evidence_copy_file( + const char *source_path, + const char *destination_directory, + const char *destination_name, + GCancellable *cancellable, + GError **error +) +{ + EvidenceCopyResult *result = NULL; + + GError *hash_error = NULL; + + char *source_sha256_before = NULL; + char *source_sha256_after = NULL; + char *destination_sha256 = NULL; + + char *destination_path = NULL; + char *destination_name_copy = NULL; + + guint64 source_size_before = 0; + guint64 source_size_after = 0; + guint64 destination_size = 0; + guint64 copied_size_bytes = 0; + + int source_descriptor = -1; + int destination_directory_descriptor = -1; + int destination_descriptor = -1; + int cleanup_errno = 0; + + gboolean destination_created = FALSE; + gboolean success = FALSE; + + g_return_val_if_fail( + error == NULL || *error == NULL, + NULL + ); + + if (source_path == NULL || + source_path[0] == '\0') + { + evidence_copy_set_error_literal( + error, + EVIDENCE_COPY_ERROR_INVALID_ARGUMENT, + "Le chemin du fichier source est invalide." + ); + + return NULL; + } + + if (destination_directory == NULL || + destination_directory[0] == '\0') + { + evidence_copy_set_error_literal( + error, + EVIDENCE_COPY_ERROR_INVALID_ARGUMENT, + "Le dossier de destination est invalide." + ); + + return NULL; + } + + if (!evidence_copy_destination_name_is_valid( + destination_name + )) + { + evidence_copy_set_error_literal( + error, + EVIDENCE_COPY_ERROR_INVALID_ARGUMENT, + "Le nom du fichier destination est invalide." + ); + + return NULL; + } + + if (evidence_copy_check_cancelled( + cancellable, + error + )) + { + return NULL; + } + + /* + * Première photographie cryptographique du fichier source. + */ + if (!file_hash_compute_sha256( + source_path, + cancellable, + &source_sha256_before, + &source_size_before, + &hash_error + )) + { + evidence_copy_set_hash_error( + error, + hash_error + ); + + g_clear_error( + &hash_error + ); + + return NULL; + } + + destination_path = + g_build_filename( + destination_directory, + destination_name, + NULL + ); + + if (destination_path == NULL) + { + evidence_copy_set_error_literal( + error, + EVIDENCE_COPY_ERROR_MEMORY, + "Impossible de construire le chemin de destination." + ); + + goto cleanup; + } + + source_descriptor = + evidence_copy_open_source_file( + source_path, + error + ); + + if (source_descriptor < 0) + { + goto cleanup; + } + + destination_directory_descriptor = + evidence_copy_open_destination_directory( + destination_directory, + error + ); + + if (destination_directory_descriptor < 0) + { + goto cleanup; + } + + if (evidence_copy_check_cancelled( + cancellable, + error + )) + { + goto cleanup; + } + + destination_descriptor = + evidence_copy_create_destination_file( + destination_directory_descriptor, + destination_name, + destination_path, + error + ); + + if (destination_descriptor < 0) + { + goto cleanup; + } + + destination_created = TRUE; + + if (!evidence_copy_contents( + source_descriptor, + destination_descriptor, + source_path, + destination_path, + cancellable, + &copied_size_bytes, + error + )) + { + goto cleanup; + } + + if (evidence_copy_check_cancelled( + cancellable, + error + )) + { + goto cleanup; + } + + if (fsync( + destination_descriptor + ) != 0) + { + evidence_copy_set_system_error( + error, + EVIDENCE_COPY_ERROR_SYNC, + "Impossible de synchroniser le fichier destination", + destination_path, + errno + ); + + goto cleanup; + } + + /* + * Un échec de fermeture d’un fichier écrit est considéré comme + * un échec de finalisation. + */ + if (close( + destination_descriptor + ) != 0) + { + destination_descriptor = -1; + + evidence_copy_set_system_error( + error, + EVIDENCE_COPY_ERROR_SYNC, + "Impossible de fermer le fichier destination", + destination_path, + errno + ); + + goto cleanup; + } + + destination_descriptor = -1; + + if (close( + source_descriptor + ) != 0) + { + g_warning( + "Impossible de fermer proprement le fichier source '%s' : %s", + source_path, + g_strerror(errno) + ); + } + + source_descriptor = -1; + + if (evidence_copy_check_cancelled( + cancellable, + error + )) + { + goto cleanup; + } + + evidence_copy_test_notify_before_verify( + destination_path + ); + + /* + * Vérification cryptographique de la copie. + */ + if (!file_hash_compute_sha256( + destination_path, + cancellable, + &destination_sha256, + &destination_size, + &hash_error + )) + { + if (error != NULL) + { + g_set_error( + error, + EVIDENCE_COPY_ERROR, + EVIDENCE_COPY_ERROR_HASH, + "Impossible de vérifier le fichier destination : %s", + hash_error != NULL + ? hash_error->message + : "erreur SHA-256 inconnue" + ); + } + + g_clear_error( + &hash_error + ); + + goto cleanup; + } + + if (evidence_copy_check_cancelled( + cancellable, + error + )) + { + goto cleanup; + } + + /* + * Deuxième photographie du source afin de détecter une modification + * survenue pendant la copie. + */ + if (!file_hash_compute_sha256( + source_path, + cancellable, + &source_sha256_after, + &source_size_after, + &hash_error + )) + { + evidence_copy_set_hash_error( + error, + hash_error + ); + + g_clear_error( + &hash_error + ); + + goto cleanup; + } + + if (copied_size_bytes != source_size_before || + destination_size != source_size_before || + source_size_after != source_size_before || + strcmp( + destination_sha256, + source_sha256_before + ) != 0 || + strcmp( + source_sha256_after, + source_sha256_before + ) != 0) + { + evidence_copy_set_error_literal( + error, + EVIDENCE_COPY_ERROR_VERIFY, + "La source ou sa copie a changé pendant l’opération." + ); + + goto cleanup; + } + + destination_name_copy = + evidence_copy_duplicate_string( + destination_name, + error + ); + + if (destination_name_copy == NULL) + { + goto cleanup; + } + + result = + g_try_new0( + EvidenceCopyResult, + 1 + ); + + if (result == NULL) + { + evidence_copy_set_error_literal( + error, + EVIDENCE_COPY_ERROR_MEMORY, + "Impossible d’allouer le résultat de la copie." + ); + + goto cleanup; + } + + result->destination_path = + destination_path; + + destination_path = NULL; + + result->destination_name = + destination_name_copy; + + destination_name_copy = NULL; + + result->sha256 = + destination_sha256; + + destination_sha256 = NULL; + + result->size_bytes = + destination_size; + + success = TRUE; + +cleanup: + + if (destination_descriptor >= 0) + { + if (close( + destination_descriptor + ) != 0) + { + g_warning( + "Impossible de fermer le fichier destination '%s' : %s", + destination_path != NULL + ? destination_path + : destination_name, + g_strerror(errno) + ); + } + } + + if (source_descriptor >= 0) + { + if (close( + source_descriptor + ) != 0) + { + g_warning( + "Impossible de fermer le fichier source '%s' : %s", + source_path, + g_strerror(errno) + ); + } + } + + /* + * Une destination n’est conservée qu’après validation complète. + */ + if (!success && + destination_created && + destination_directory_descriptor >= 0) + { + if (evidence_copy_remove_invalid_destination( + destination_directory_descriptor, + destination_name, + destination_path != NULL + ? destination_path + : destination_name + ) != 0) + { + cleanup_errno = errno; + + evidence_copy_report_cleanup_failure( + error, + destination_path != NULL + ? destination_path + : destination_name, + cleanup_errno + ); + } + } + + if (destination_directory_descriptor >= 0) + { + if (close( + destination_directory_descriptor + ) != 0) + { + g_warning( + "Impossible de fermer le dossier destination '%s' : %s", + destination_directory, + g_strerror(errno) + ); + } + } + + g_free( + destination_name_copy + ); + + g_free( + destination_path + ); + + g_free( + destination_sha256 + ); + + g_free( + source_sha256_after + ); + + g_free( + source_sha256_before + ); + + if (!success) + { + evidence_copy_result_free( + result + ); + + result = NULL; + } + + return result; +} + +void evidence_copy_result_free( + EvidenceCopyResult *result +) +{ + if (result == NULL) + { + return; + } + + g_free( + result->sha256 + ); + + g_free( + result->destination_name + ); + + g_free( + result->destination_path + ); + + g_free( + result + ); +} + +const char *evidence_copy_result_get_destination_path( + const EvidenceCopyResult *result +) +{ + if (result == NULL) + { + return NULL; + } + + return result->destination_path; +} + +const char *evidence_copy_result_get_destination_name( + const EvidenceCopyResult *result +) +{ + if (result == NULL) + { + return NULL; + } + + return result->destination_name; +} + +const char *evidence_copy_result_get_sha256( + const EvidenceCopyResult *result +) +{ + if (result == NULL) + { + return NULL; + } + + return result->sha256; +} + +guint64 evidence_copy_result_get_size_bytes( + const EvidenceCopyResult *result +) +{ + if (result == NULL) + { + return 0; + } + + return result->size_bytes; +} diff --git a/tests/test_evidence_copy b/tests/test_evidence_copy new file mode 100755 index 0000000..402f688 Binary files /dev/null and b/tests/test_evidence_copy differ diff --git a/tests/test_evidence_copy.c b/tests/test_evidence_copy.c new file mode 100644 index 0000000..3916f2e --- /dev/null +++ b/tests/test_evidence_copy.c @@ -0,0 +1,3194 @@ +/****************************************************************************** + * @file test_evidence_copy.c + * @brief Tests de la copie sûre des fichiers de preuve. + ******************************************************************************/ + +#define _POSIX_C_SOURCE 200809L + +#ifndef EVIDENCE_COPY_ENABLE_TEST_HOOKS +#define EVIDENCE_COPY_ENABLE_TEST_HOOKS +#endif + +#include "core/evidence_copy.h" + +#include +#include +#include +#include + +#include +#include +#include + +/** + * @brief Environnement temporaire des tests EvidenceCopy. + */ +typedef struct +{ + char *root_directory; + char *source_directory; + char *destination_directory; +} TestEvidenceCopyFixture; + +/** + * @brief Contexte d’annulation pendant la copie. + */ +typedef struct +{ + GCancellable *cancellable; + guint block_count; +} TestEvidenceCopyCancellationContext; + +/** + * @brief Contexte d’une erreur d’écriture simulée. + */ +typedef struct +{ + guint64 fail_after_size_bytes; + guint call_count; +} TestEvidenceCopyWriteFailureContext; + +/** + * @brief Contexte de corruption avant vérification. + */ +typedef struct +{ + guint call_count; +} TestEvidenceCopyVerifyFailureContext; + +/** + * @brief Contexte de modification du fichier source. + */ +typedef struct +{ + const char *source_path; + guint call_count; +} TestEvidenceCopySourceModificationContext; + +/** + * @brief Contexte d’un échec de nettoyage simulé. + */ +typedef struct +{ + guint call_count; +} TestEvidenceCopyCleanupFailureContext; + +/** + * @brief Crée un environnement temporaire isolé. + */ +static TestEvidenceCopyFixture +test_evidence_copy_fixture_create(void) +{ + TestEvidenceCopyFixture fixture = {0}; + + GError *error = NULL; + + fixture.root_directory = + g_dir_make_tmp( + "labfy-evidence-copy-test-XXXXXX", + &error + ); + + assert(error == NULL); + assert(fixture.root_directory != NULL); + + fixture.source_directory = + g_build_filename( + fixture.root_directory, + "source", + NULL + ); + + fixture.destination_directory = + g_build_filename( + fixture.root_directory, + "destination", + NULL + ); + + assert(fixture.source_directory != NULL); + assert(fixture.destination_directory != NULL); + + assert( + g_mkdir( + fixture.source_directory, + 0700 + ) == 0 + ); + + assert( + g_mkdir( + fixture.destination_directory, + 0700 + ) == 0 + ); + + return fixture; +} + +/** + * @brief Vérifie une erreur EvidenceCopy. + */ +static void test_evidence_copy_assert_error( + const GError *error, + EvidenceCopyError expected_error +) +{ + assert(error != NULL); + assert(error->domain == EVIDENCE_COPY_ERROR); + + assert( + error->code == + (gint) expected_error + ); + + assert(error->message != NULL); + assert(error->message[0] != '\0'); +} + +/** + * @brief Supprime un environnement temporaire vide. + */ +static void test_evidence_copy_fixture_clear( + TestEvidenceCopyFixture *fixture +) +{ + assert(fixture != NULL); + assert(fixture->root_directory != NULL); + assert(fixture->source_directory != NULL); + assert(fixture->destination_directory != NULL); + + assert( + g_rmdir( + fixture->destination_directory + ) == 0 + ); + + assert( + g_rmdir( + fixture->source_directory + ) == 0 + ); + + assert( + g_rmdir( + fixture->root_directory + ) == 0 + ); + + g_free( + fixture->destination_directory + ); + + g_free( + fixture->source_directory + ); + + g_free( + fixture->root_directory + ); + + fixture->destination_directory = NULL; + fixture->source_directory = NULL; + fixture->root_directory = NULL; +} + +/** + * @brief Construit un chemin dans le dossier source. + */ +static char *test_evidence_copy_build_source_path( + const TestEvidenceCopyFixture *fixture, + const char *file_name +) +{ + assert(fixture != NULL); + assert(fixture->source_directory != NULL); + assert(file_name != NULL); + + return g_build_filename( + fixture->source_directory, + file_name, + NULL + ); +} + +/** + * @brief Construit un chemin dans le dossier destination. + */ +static char *test_evidence_copy_build_destination_path( + const TestEvidenceCopyFixture *fixture, + const char *file_name +) +{ + assert(fixture != NULL); + assert(fixture->destination_directory != NULL); + assert(file_name != NULL); + + return g_build_filename( + fixture->destination_directory, + file_name, + NULL + ); +} + +/** + * @brief Vérifie les accesseurs avec un résultat NULL. + */ +static void test_evidence_copy_null_result(void) +{ + assert( + evidence_copy_result_get_destination_path( + NULL + ) == NULL + ); + + assert( + evidence_copy_result_get_destination_name( + NULL + ) == NULL + ); + + assert( + evidence_copy_result_get_sha256( + NULL + ) == NULL + ); + + assert( + evidence_copy_result_get_size_bytes( + NULL + ) == 0 + ); + + evidence_copy_result_free( + NULL + ); +} + +/** + * @brief Vérifie la copie complète du contenu connu « abc ». + */ +static void test_evidence_copy_text_file(void) +{ + TestEvidenceCopyFixture fixture = + test_evidence_copy_fixture_create(); + + EvidenceCopyResult *result = NULL; + + char *source_path = NULL; + char *destination_path = NULL; + char *destination_name = NULL; + + char *source_contents = NULL; + char *destination_contents = NULL; + + gsize source_contents_size = 0; + gsize destination_contents_size = 0; + + GError *error = NULL; + + source_path = + test_evidence_copy_build_source_path( + &fixture, + "original.txt" + ); + + destination_path = + test_evidence_copy_build_destination_path( + &fixture, + "copie.txt" + ); + + destination_name = + g_strdup( + "copie.txt" + ); + + assert(source_path != NULL); + assert(destination_path != NULL); + assert(destination_name != NULL); + + assert( + g_file_set_contents( + source_path, + "abc", + 3, + &error + ) + ); + + assert(error == NULL); + + result = + evidence_copy_file( + source_path, + fixture.destination_directory, + destination_name, + NULL, + &error + ); + + assert(error == NULL); + assert(result != NULL); + + /* + * La chaîne fournie par l’appelant peut être libérée : + * le résultat doit posséder sa propre copie. + */ + g_free( + destination_name + ); + + destination_name = NULL; + + assert( + strcmp( + evidence_copy_result_get_destination_path( + result + ), + destination_path + ) == 0 + ); + + assert( + strcmp( + evidence_copy_result_get_destination_name( + result + ), + "copie.txt" + ) == 0 + ); + + assert( + strcmp( + evidence_copy_result_get_sha256( + result + ), + "ba7816bf8f01cfea414140de5dae2223" + "b00361a396177a9cb410ff61f20015ad" + ) == 0 + ); + + assert( + evidence_copy_result_get_size_bytes( + result + ) == 3 + ); + + /* + * Libérer le résultat ne doit pas supprimer la copie validée. + */ + evidence_copy_result_free( + result + ); + + result = NULL; + + assert( + g_file_test( + destination_path, + G_FILE_TEST_IS_REGULAR + ) + ); + + assert( + g_file_get_contents( + source_path, + &source_contents, + &source_contents_size, + &error + ) + ); + + assert(error == NULL); + + assert( + g_file_get_contents( + destination_path, + &destination_contents, + &destination_contents_size, + &error + ) + ); + + assert(error == NULL); + + assert(source_contents_size == 3); + assert(destination_contents_size == 3); + + assert( + memcmp( + source_contents, + destination_contents, + 3 + ) == 0 + ); + + assert( + memcmp( + source_contents, + "abc", + 3 + ) == 0 + ); + + g_free( + destination_contents + ); + + g_free( + source_contents + ); + + assert( + g_remove( + destination_path + ) == 0 + ); + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + destination_path + ); + + g_free( + source_path + ); + + test_evidence_copy_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie la copie d’un fichier vide. + */ +static void test_evidence_copy_empty_file(void) +{ + TestEvidenceCopyFixture fixture = + test_evidence_copy_fixture_create(); + + EvidenceCopyResult *result = NULL; + + char *source_path = NULL; + char *destination_path = NULL; + char *destination_contents = NULL; + + gsize destination_contents_size = 42; + + GError *error = NULL; + + source_path = + test_evidence_copy_build_source_path( + &fixture, + "empty.bin" + ); + + destination_path = + test_evidence_copy_build_destination_path( + &fixture, + "empty-copy.bin" + ); + + assert(source_path != NULL); + assert(destination_path != NULL); + + assert( + g_file_set_contents( + source_path, + "", + 0, + &error + ) + ); + + assert(error == NULL); + + result = + evidence_copy_file( + source_path, + fixture.destination_directory, + "empty-copy.bin", + NULL, + &error + ); + + assert(error == NULL); + assert(result != NULL); + + assert( + evidence_copy_result_get_size_bytes( + result + ) == 0 + ); + + assert( + strcmp( + evidence_copy_result_get_sha256( + result + ), + "e3b0c44298fc1c149afbf4c8996fb924" + "27ae41e4649b934ca495991b7852b855" + ) == 0 + ); + + assert( + strcmp( + evidence_copy_result_get_destination_path( + result + ), + destination_path + ) == 0 + ); + + evidence_copy_result_free( + result + ); + + result = NULL; + + assert( + g_file_test( + destination_path, + G_FILE_TEST_IS_REGULAR + ) + ); + + assert( + g_file_get_contents( + destination_path, + &destination_contents, + &destination_contents_size, + &error + ) + ); + + assert(error == NULL); + assert(destination_contents != NULL); + assert(destination_contents_size == 0); + + g_free( + destination_contents + ); + + assert( + g_remove( + destination_path + ) == 0 + ); + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + destination_path + ); + + g_free( + source_path + ); + + test_evidence_copy_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie la copie exacte de données binaires. + */ +static void test_evidence_copy_binary_file(void) +{ + TestEvidenceCopyFixture fixture = + test_evidence_copy_fixture_create(); + + static const guint8 binary_data[] = + { + 0x00, + 0xFF, + 0x80, + 0x41, + 0x00, + 0x7F, + 0x10 + }; + + EvidenceCopyResult *result = NULL; + + char *source_path = NULL; + char *destination_path = NULL; + char *destination_contents = NULL; + + gsize destination_contents_size = 0; + + GError *error = NULL; + + source_path = + test_evidence_copy_build_source_path( + &fixture, + "binary-source.bin" + ); + + destination_path = + test_evidence_copy_build_destination_path( + &fixture, + "binary-copy.bin" + ); + + assert(source_path != NULL); + assert(destination_path != NULL); + + assert( + g_file_set_contents( + source_path, + (const char *) binary_data, + (gssize) sizeof(binary_data), + &error + ) + ); + + assert(error == NULL); + + result = + evidence_copy_file( + source_path, + fixture.destination_directory, + "binary-copy.bin", + NULL, + &error + ); + + assert(error == NULL); + assert(result != NULL); + + assert( + evidence_copy_result_get_size_bytes( + result + ) == sizeof(binary_data) + ); + + assert( + strcmp( + evidence_copy_result_get_sha256( + result + ), + "c90d3a7754f84abc867e780e5a0c0f8d" + "a5c663bd1766ad89e538536a3763f367" + ) == 0 + ); + + evidence_copy_result_free( + result + ); + + assert( + g_file_get_contents( + destination_path, + &destination_contents, + &destination_contents_size, + &error + ) + ); + + assert(error == NULL); + + assert( + destination_contents_size == + sizeof(binary_data) + ); + + assert( + memcmp( + destination_contents, + binary_data, + sizeof(binary_data) + ) == 0 + ); + + g_free( + destination_contents + ); + + assert( + g_remove( + destination_path + ) == 0 + ); + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + destination_path + ); + + g_free( + source_path + ); + + test_evidence_copy_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie la copie d’un fichier supérieur au tampon. + */ +static void test_evidence_copy_multiple_blocks(void) +{ + TestEvidenceCopyFixture fixture = + test_evidence_copy_fixture_create(); + + const gsize data_size = + (200U * 1024U) + 123U; + + EvidenceCopyResult *result = NULL; + + guint8 *source_data = NULL; + + char *source_path = NULL; + char *destination_path = NULL; + char *destination_contents = NULL; + + gsize destination_contents_size = 0; + gsize data_index = 0; + + GError *error = NULL; + + source_path = + test_evidence_copy_build_source_path( + &fixture, + "large-source.bin" + ); + + destination_path = + test_evidence_copy_build_destination_path( + &fixture, + "large-copy.bin" + ); + + assert(source_path != NULL); + assert(destination_path != NULL); + + source_data = + g_malloc( + data_size + ); + + assert(source_data != NULL); + + for (data_index = 0; + data_index < data_size; + data_index++) + { + source_data[data_index] = + (guint8) (data_index % 251U); + } + + assert( + g_file_set_contents( + source_path, + (const char *) source_data, + (gssize) data_size, + &error + ) + ); + + assert(error == NULL); + + result = + evidence_copy_file( + source_path, + fixture.destination_directory, + "large-copy.bin", + NULL, + &error + ); + + assert(error == NULL); + assert(result != NULL); + + assert( + evidence_copy_result_get_size_bytes( + result + ) == data_size + ); + + assert( + strcmp( + evidence_copy_result_get_sha256( + result + ), + "bc5ad58e926b1f79d9840418910f47ba" + "1e947fb137170b8d5403a537b888c4af" + ) == 0 + ); + + evidence_copy_result_free( + result + ); + + assert( + g_file_get_contents( + destination_path, + &destination_contents, + &destination_contents_size, + &error + ) + ); + + assert(error == NULL); + assert(destination_contents_size == data_size); + + assert( + memcmp( + destination_contents, + source_data, + data_size + ) == 0 + ); + + g_free( + destination_contents + ); + + g_free( + source_data + ); + + assert( + g_remove( + destination_path + ) == 0 + ); + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + destination_path + ); + + g_free( + source_path + ); + + test_evidence_copy_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie les permissions restrictives de la copie. + */ +static void test_evidence_copy_permissions(void) +{ + TestEvidenceCopyFixture fixture = + test_evidence_copy_fixture_create(); + + EvidenceCopyResult *result = NULL; + + struct stat destination_status; + + char *source_path = NULL; + char *destination_path = NULL; + + GError *error = NULL; + + source_path = + test_evidence_copy_build_source_path( + &fixture, + "permissions-source.bin" + ); + + destination_path = + test_evidence_copy_build_destination_path( + &fixture, + "permissions-copy.bin" + ); + + assert(source_path != NULL); + assert(destination_path != NULL); + + assert( + g_file_set_contents( + source_path, + "preuve", + 6, + &error + ) + ); + + assert(error == NULL); + + result = + evidence_copy_file( + source_path, + fixture.destination_directory, + "permissions-copy.bin", + NULL, + &error + ); + + assert(error == NULL); + assert(result != NULL); + + assert( + stat( + destination_path, + &destination_status + ) == 0 + ); + + assert( + S_ISREG( + destination_status.st_mode + ) + ); + + /* + * Aucun droit pour le groupe ou les autres utilisateurs. + */ + assert( + (destination_status.st_mode & 0077) == 0 + ); + + evidence_copy_result_free( + result + ); + + assert( + g_remove( + destination_path + ) == 0 + ); + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + destination_path + ); + + g_free( + source_path + ); + + test_evidence_copy_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie qu’un fichier existant n’est jamais écrasé. + */ +static void test_evidence_copy_destination_exists(void) +{ + TestEvidenceCopyFixture fixture = + test_evidence_copy_fixture_create(); + + EvidenceCopyResult *result = NULL; + + char *source_path = NULL; + char *destination_path = NULL; + + char *source_contents = NULL; + char *destination_contents = NULL; + + gsize source_contents_size = 0; + gsize destination_contents_size = 0; + + GError *error = NULL; + + source_path = + test_evidence_copy_build_source_path( + &fixture, + "existing-source.bin" + ); + + destination_path = + test_evidence_copy_build_destination_path( + &fixture, + "existing-copy.bin" + ); + + assert(source_path != NULL); + assert(destination_path != NULL); + + assert( + g_file_set_contents( + source_path, + "nouvelle preuve", + 15, + &error + ) + ); + + assert(error == NULL); + + assert( + g_file_set_contents( + destination_path, + "ne pas modifier", + 15, + &error + ) + ); + + assert(error == NULL); + + result = + evidence_copy_file( + source_path, + fixture.destination_directory, + "existing-copy.bin", + NULL, + &error + ); + + assert(result == NULL); + + test_evidence_copy_assert_error( + error, + EVIDENCE_COPY_ERROR_DESTINATION_EXISTS + ); + + g_clear_error( + &error + ); + + assert( + g_file_get_contents( + source_path, + &source_contents, + &source_contents_size, + &error + ) + ); + + assert(error == NULL); + + assert( + g_file_get_contents( + destination_path, + &destination_contents, + &destination_contents_size, + &error + ) + ); + + assert(error == NULL); + + assert(source_contents_size == 15); + assert(destination_contents_size == 15); + + assert( + memcmp( + source_contents, + "nouvelle preuve", + 15 + ) == 0 + ); + + assert( + memcmp( + destination_contents, + "ne pas modifier", + 15 + ) == 0 + ); + + g_free( + destination_contents + ); + + g_free( + source_contents + ); + + assert( + g_remove( + destination_path + ) == 0 + ); + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + destination_path + ); + + g_free( + source_path + ); + + test_evidence_copy_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie qu’aucun fichier destination n’a été créé. + */ +static void test_evidence_copy_assert_destination_absent( + const char *destination_path +) +{ + assert(destination_path != NULL); + + assert( + !g_file_test( + destination_path, + G_FILE_TEST_EXISTS + ) + ); +} + +/** + * @brief Vérifie le refus des paramètres obligatoires invalides. + */ +static void test_evidence_copy_invalid_arguments(void) +{ + TestEvidenceCopyFixture fixture = + test_evidence_copy_fixture_create(); + + EvidenceCopyResult *result = NULL; + + GError *error = NULL; + + result = + evidence_copy_file( + NULL, + fixture.destination_directory, + "copy.bin", + NULL, + &error + ); + + assert(result == NULL); + + test_evidence_copy_assert_error( + error, + EVIDENCE_COPY_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + result = + evidence_copy_file( + "", + fixture.destination_directory, + "copy.bin", + NULL, + &error + ); + + assert(result == NULL); + + test_evidence_copy_assert_error( + error, + EVIDENCE_COPY_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + result = + evidence_copy_file( + "/tmp/source.bin", + NULL, + "copy.bin", + NULL, + &error + ); + + assert(result == NULL); + + test_evidence_copy_assert_error( + error, + EVIDENCE_COPY_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + result = + evidence_copy_file( + "/tmp/source.bin", + "", + "copy.bin", + NULL, + &error + ); + + assert(result == NULL); + + test_evidence_copy_assert_error( + error, + EVIDENCE_COPY_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + test_evidence_copy_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus des noms pouvant représenter un chemin. + */ +static void test_evidence_copy_invalid_destination_names(void) +{ + TestEvidenceCopyFixture fixture = + test_evidence_copy_fixture_create(); + + static const char *invalid_names[] = + { + NULL, + "", + ".", + "..", + "subdirectory/copy.bin", + "subdirectory\\copy.bin" + }; + + EvidenceCopyResult *result = NULL; + + char *source_path = NULL; + + gsize name_index = 0; + + GError *error = NULL; + + source_path = + test_evidence_copy_build_source_path( + &fixture, + "valid-source.bin" + ); + + assert(source_path != NULL); + + assert( + g_file_set_contents( + source_path, + "source", + 6, + &error + ) + ); + + assert(error == NULL); + + for (name_index = 0; + name_index < G_N_ELEMENTS(invalid_names); + name_index++) + { + result = + evidence_copy_file( + source_path, + fixture.destination_directory, + invalid_names[name_index], + NULL, + &error + ); + + assert(result == NULL); + + test_evidence_copy_assert_error( + error, + EVIDENCE_COPY_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + } + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + source_path + ); + + test_evidence_copy_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus d’un fichier source absent. + */ +static void test_evidence_copy_missing_source(void) +{ + TestEvidenceCopyFixture fixture = + test_evidence_copy_fixture_create(); + + EvidenceCopyResult *result = NULL; + + char *source_path = NULL; + char *destination_path = NULL; + + GError *error = NULL; + + source_path = + test_evidence_copy_build_source_path( + &fixture, + "missing.bin" + ); + + destination_path = + test_evidence_copy_build_destination_path( + &fixture, + "copy.bin" + ); + + assert(source_path != NULL); + assert(destination_path != NULL); + + result = + evidence_copy_file( + source_path, + fixture.destination_directory, + "copy.bin", + NULL, + &error + ); + + assert(result == NULL); + + test_evidence_copy_assert_error( + error, + EVIDENCE_COPY_ERROR_SOURCE_NOT_FOUND + ); + + g_clear_error( + &error + ); + + test_evidence_copy_assert_destination_absent( + destination_path + ); + + g_free( + destination_path + ); + + g_free( + source_path + ); + + test_evidence_copy_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie qu’un dossier ne peut pas devenir une preuve. + */ +static void test_evidence_copy_source_directory(void) +{ + TestEvidenceCopyFixture fixture = + test_evidence_copy_fixture_create(); + + EvidenceCopyResult *result = NULL; + + char *destination_path = NULL; + + GError *error = NULL; + + destination_path = + test_evidence_copy_build_destination_path( + &fixture, + "copy.bin" + ); + + assert(destination_path != NULL); + + result = + evidence_copy_file( + fixture.source_directory, + fixture.destination_directory, + "copy.bin", + NULL, + &error + ); + + assert(result == NULL); + + test_evidence_copy_assert_error( + error, + EVIDENCE_COPY_ERROR_SOURCE_NOT_REGULAR + ); + + g_clear_error( + &error + ); + + test_evidence_copy_assert_destination_absent( + destination_path + ); + + g_free( + destination_path + ); + + test_evidence_copy_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie qu’un lien symbolique source n’est jamais suivi. + */ +static void test_evidence_copy_source_symbolic_link(void) +{ + TestEvidenceCopyFixture fixture = + test_evidence_copy_fixture_create(); + + EvidenceCopyResult *result = NULL; + + char *target_path = NULL; + char *link_path = NULL; + char *destination_path = NULL; + + GError *error = NULL; + + target_path = + test_evidence_copy_build_source_path( + &fixture, + "target.bin" + ); + + link_path = + test_evidence_copy_build_source_path( + &fixture, + "source-link.bin" + ); + + destination_path = + test_evidence_copy_build_destination_path( + &fixture, + "copy.bin" + ); + + assert(target_path != NULL); + assert(link_path != NULL); + assert(destination_path != NULL); + + assert( + g_file_set_contents( + target_path, + "secret", + 6, + &error + ) + ); + + assert(error == NULL); + + assert( + symlink( + target_path, + link_path + ) == 0 + ); + + result = + evidence_copy_file( + link_path, + fixture.destination_directory, + "copy.bin", + NULL, + &error + ); + + assert(result == NULL); + + test_evidence_copy_assert_error( + error, + EVIDENCE_COPY_ERROR_SOURCE_NOT_REGULAR + ); + + g_clear_error( + &error + ); + + test_evidence_copy_assert_destination_absent( + destination_path + ); + + assert( + g_remove( + link_path + ) == 0 + ); + + assert( + g_remove( + target_path + ) == 0 + ); + + g_free( + destination_path + ); + + g_free( + link_path + ); + + g_free( + target_path + ); + + test_evidence_copy_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie qu’un tube nommé est refusé sans blocage. + */ +static void test_evidence_copy_source_named_pipe(void) +{ + TestEvidenceCopyFixture fixture = + test_evidence_copy_fixture_create(); + + EvidenceCopyResult *result = NULL; + + char *pipe_path = NULL; + char *destination_path = NULL; + + GError *error = NULL; + + pipe_path = + test_evidence_copy_build_source_path( + &fixture, + "source.fifo" + ); + + destination_path = + test_evidence_copy_build_destination_path( + &fixture, + "copy.bin" + ); + + assert(pipe_path != NULL); + assert(destination_path != NULL); + + assert( + mkfifo( + pipe_path, + 0600 + ) == 0 + ); + + result = + evidence_copy_file( + pipe_path, + fixture.destination_directory, + "copy.bin", + NULL, + &error + ); + + assert(result == NULL); + + test_evidence_copy_assert_error( + error, + EVIDENCE_COPY_ERROR_SOURCE_NOT_REGULAR + ); + + g_clear_error( + &error + ); + + test_evidence_copy_assert_destination_absent( + destination_path + ); + + assert( + g_remove( + pipe_path + ) == 0 + ); + + g_free( + destination_path + ); + + g_free( + pipe_path + ); + + test_evidence_copy_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie une annulation demandée avant l’import. + */ +static void test_evidence_copy_cancelled_before_start(void) +{ + TestEvidenceCopyFixture fixture = + test_evidence_copy_fixture_create(); + + EvidenceCopyResult *result = NULL; + + GCancellable *cancellable = NULL; + + char *source_path = NULL; + char *destination_path = NULL; + + GError *error = NULL; + + source_path = + test_evidence_copy_build_source_path( + &fixture, + "cancelled-source.bin" + ); + + destination_path = + test_evidence_copy_build_destination_path( + &fixture, + "cancelled-copy.bin" + ); + + assert(source_path != NULL); + assert(destination_path != NULL); + + assert( + g_file_set_contents( + source_path, + "source stable", + 13, + &error + ) + ); + + assert(error == NULL); + + cancellable = + g_cancellable_new(); + + assert(cancellable != NULL); + + g_cancellable_cancel( + cancellable + ); + + result = + evidence_copy_file( + source_path, + fixture.destination_directory, + "cancelled-copy.bin", + cancellable, + &error + ); + + assert(result == NULL); + + test_evidence_copy_assert_error( + error, + EVIDENCE_COPY_ERROR_CANCELLED + ); + + g_clear_error( + &error + ); + + test_evidence_copy_assert_destination_absent( + destination_path + ); + + g_object_unref( + cancellable + ); + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + destination_path + ); + + g_free( + source_path + ); + + test_evidence_copy_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus d’un dossier destination absent. + */ +static void test_evidence_copy_missing_destination_directory(void) +{ + TestEvidenceCopyFixture fixture = + test_evidence_copy_fixture_create(); + + EvidenceCopyResult *result = NULL; + + char *source_path = NULL; + char *missing_directory = NULL; + char *destination_path = NULL; + + GError *error = NULL; + + source_path = + test_evidence_copy_build_source_path( + &fixture, + "source.bin" + ); + + missing_directory = + g_build_filename( + fixture.root_directory, + "missing-destination", + NULL + ); + + destination_path = + g_build_filename( + missing_directory, + "copy.bin", + NULL + ); + + assert(source_path != NULL); + assert(missing_directory != NULL); + assert(destination_path != NULL); + + assert( + g_file_set_contents( + source_path, + "preuve", + 6, + &error + ) + ); + + assert(error == NULL); + + result = + evidence_copy_file( + source_path, + missing_directory, + "copy.bin", + NULL, + &error + ); + + assert(result == NULL); + + test_evidence_copy_assert_error( + error, + EVIDENCE_COPY_ERROR_DESTINATION_INVALID + ); + + g_clear_error( + &error + ); + + test_evidence_copy_assert_destination_absent( + destination_path + ); + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + destination_path + ); + + g_free( + missing_directory + ); + + g_free( + source_path + ); + + test_evidence_copy_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie qu’un fichier ne peut pas servir de dossier destination. + */ +static void test_evidence_copy_destination_is_file(void) +{ + TestEvidenceCopyFixture fixture = + test_evidence_copy_fixture_create(); + + EvidenceCopyResult *result = NULL; + + char *source_path = NULL; + char *fake_directory_path = NULL; + + char *fake_directory_contents = NULL; + gsize fake_directory_contents_size = 0; + + GError *error = NULL; + + source_path = + test_evidence_copy_build_source_path( + &fixture, + "source.bin" + ); + + fake_directory_path = + g_build_filename( + fixture.root_directory, + "not-a-directory", + NULL + ); + + assert(source_path != NULL); + assert(fake_directory_path != NULL); + + assert( + g_file_set_contents( + source_path, + "preuve", + 6, + &error + ) + ); + + assert(error == NULL); + + assert( + g_file_set_contents( + fake_directory_path, + "contenu intact", + 14, + &error + ) + ); + + assert(error == NULL); + + result = + evidence_copy_file( + source_path, + fake_directory_path, + "copy.bin", + NULL, + &error + ); + + assert(result == NULL); + + test_evidence_copy_assert_error( + error, + EVIDENCE_COPY_ERROR_DESTINATION_INVALID + ); + + g_clear_error( + &error + ); + + /* + * Le fichier pris pour un dossier doit rester intact. + */ + assert( + g_file_get_contents( + fake_directory_path, + &fake_directory_contents, + &fake_directory_contents_size, + &error + ) + ); + + assert(error == NULL); + assert(fake_directory_contents_size == 14); + + assert( + memcmp( + fake_directory_contents, + "contenu intact", + 14 + ) == 0 + ); + + g_free( + fake_directory_contents + ); + + assert( + g_remove( + fake_directory_path + ) == 0 + ); + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + fake_directory_path + ); + + g_free( + source_path + ); + + test_evidence_copy_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie qu’un dossier destination symbolique n’est pas suivi. + */ +static void test_evidence_copy_destination_symbolic_link(void) +{ + TestEvidenceCopyFixture fixture = + test_evidence_copy_fixture_create(); + + EvidenceCopyResult *result = NULL; + + char *source_path = NULL; + char *link_path = NULL; + char *real_destination_path = NULL; + + GError *error = NULL; + + source_path = + test_evidence_copy_build_source_path( + &fixture, + "source.bin" + ); + + link_path = + g_build_filename( + fixture.root_directory, + "destination-link", + NULL + ); + + real_destination_path = + test_evidence_copy_build_destination_path( + &fixture, + "copy.bin" + ); + + assert(source_path != NULL); + assert(link_path != NULL); + assert(real_destination_path != NULL); + + assert( + g_file_set_contents( + source_path, + "preuve", + 6, + &error + ) + ); + + assert(error == NULL); + + assert( + symlink( + fixture.destination_directory, + link_path + ) == 0 + ); + + result = + evidence_copy_file( + source_path, + link_path, + "copy.bin", + NULL, + &error + ); + + assert(result == NULL); + + test_evidence_copy_assert_error( + error, + EVIDENCE_COPY_ERROR_DESTINATION_INVALID + ); + + g_clear_error( + &error + ); + + /* + * La cible réelle du lien ne doit recevoir aucun fichier. + */ + test_evidence_copy_assert_destination_absent( + real_destination_path + ); + + assert( + g_remove( + link_path + ) == 0 + ); + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + real_destination_path + ); + + g_free( + link_path + ); + + g_free( + source_path + ); + + test_evidence_copy_fixture_clear( + &fixture + ); +} + +/** + * @brief Annule la copie après le premier bloc. + */ +static void test_evidence_copy_cancel_after_first_block( + guint64 copied_size_bytes, + gpointer user_data +) +{ + TestEvidenceCopyCancellationContext *context = + user_data; + + assert(context != NULL); + assert(context->cancellable != NULL); + assert(copied_size_bytes > 0); + + context->block_count++; + + if (context->block_count == 1) + { + g_cancellable_cancel( + context->cancellable + ); + } +} + +/** + * @brief Vérifie l’annulation pendant une copie en cours. + */ +static void test_evidence_copy_cancelled_during_copy(void) +{ + TestEvidenceCopyFixture fixture = + test_evidence_copy_fixture_create(); + + TestEvidenceCopyCancellationContext context = {0}; + + const gsize data_size = + 256U * 1024U; + + EvidenceCopyResult *result = NULL; + + guint8 *source_data = NULL; + + char *source_path = NULL; + char *destination_path = NULL; + + gsize data_index = 0; + + GError *error = NULL; + + source_path = + test_evidence_copy_build_source_path( + &fixture, + "cancel-source.bin" + ); + + destination_path = + test_evidence_copy_build_destination_path( + &fixture, + "cancel-copy.bin" + ); + + assert(source_path != NULL); + assert(destination_path != NULL); + + source_data = + g_malloc( + data_size + ); + + assert(source_data != NULL); + + for (data_index = 0; + data_index < data_size; + data_index++) + { + source_data[data_index] = + (guint8) (data_index % 251U); + } + + assert( + g_file_set_contents( + source_path, + (const char *) source_data, + (gssize) data_size, + &error + ) + ); + + assert(error == NULL); + + context.cancellable = + g_cancellable_new(); + + assert(context.cancellable != NULL); + + evidence_copy_test_set_block_hook( + test_evidence_copy_cancel_after_first_block, + &context + ); + + result = + evidence_copy_file( + source_path, + fixture.destination_directory, + "cancel-copy.bin", + context.cancellable, + &error + ); + + /* + * Toujours retirer le crochet avant les assertions suivantes. + */ + evidence_copy_test_set_block_hook( + NULL, + NULL + ); + + assert(result == NULL); + assert(context.block_count == 1); + + test_evidence_copy_assert_error( + error, + EVIDENCE_COPY_ERROR_CANCELLED + ); + + g_clear_error( + &error + ); + + /* + * La copie partielle doit avoir été supprimée. + */ + test_evidence_copy_assert_destination_absent( + destination_path + ); + + /* + * Le fichier source doit rester intact. + */ + assert( + g_file_test( + source_path, + G_FILE_TEST_IS_REGULAR + ) + ); + + g_object_unref( + context.cancellable + ); + + g_free( + source_data + ); + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + destination_path + ); + + g_free( + source_path + ); + + test_evidence_copy_fixture_clear( + &fixture + ); +} + +/** + * @brief Déclenche une erreur après la quantité demandée. + */ +static gboolean test_evidence_copy_fail_write_after_size( + guint64 written_size_bytes, + gpointer user_data +) +{ + TestEvidenceCopyWriteFailureContext *context = + user_data; + + assert(context != NULL); + + context->call_count++; + + return written_size_bytes >= + context->fail_after_size_bytes; +} + +/** + * @brief Vérifie le nettoyage après une erreur d’écriture. + */ +static void test_evidence_copy_write_failure_cleanup(void) +{ + TestEvidenceCopyFixture fixture = + test_evidence_copy_fixture_create(); + + TestEvidenceCopyWriteFailureContext context = {0}; + + const gsize data_size = + 256U * 1024U; + + EvidenceCopyResult *result = NULL; + + guint8 *source_data = NULL; + + char *source_path = NULL; + char *destination_path = NULL; + char *source_contents = NULL; + + gsize data_index = 0; + gsize source_contents_size = 0; + + GError *error = NULL; + + source_path = + test_evidence_copy_build_source_path( + &fixture, + "write-failure-source.bin" + ); + + destination_path = + test_evidence_copy_build_destination_path( + &fixture, + "write-failure-copy.bin" + ); + + assert(source_path != NULL); + assert(destination_path != NULL); + + source_data = + g_malloc( + data_size + ); + + assert(source_data != NULL); + + for (data_index = 0; + data_index < data_size; + data_index++) + { + source_data[data_index] = + (guint8) (data_index % 251U); + } + + assert( + g_file_set_contents( + source_path, + (const char *) source_data, + (gssize) data_size, + &error + ) + ); + + assert(error == NULL); + + /* + * Le premier bloc de 64 Kio est écrit. + * L’écriture suivante doit échouer. + */ + context.fail_after_size_bytes = + 64U * 1024U; + + evidence_copy_test_set_before_write_hook( + test_evidence_copy_fail_write_after_size, + &context + ); + + result = + evidence_copy_file( + source_path, + fixture.destination_directory, + "write-failure-copy.bin", + NULL, + &error + ); + + evidence_copy_test_set_before_write_hook( + NULL, + NULL + ); + + assert(result == NULL); + assert(context.call_count >= 2); + + test_evidence_copy_assert_error( + error, + EVIDENCE_COPY_ERROR_WRITE + ); + + g_clear_error( + &error + ); + + /* + * Le fichier partiel ne doit plus exister. + */ + test_evidence_copy_assert_destination_absent( + destination_path + ); + + /* + * Le fichier source doit rester strictement intact. + */ + assert( + g_file_get_contents( + source_path, + &source_contents, + &source_contents_size, + &error + ) + ); + + assert(error == NULL); + assert(source_contents_size == data_size); + + assert( + memcmp( + source_contents, + source_data, + data_size + ) == 0 + ); + + g_free( + source_contents + ); + + g_free( + source_data + ); + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + destination_path + ); + + g_free( + source_path + ); + + test_evidence_copy_fixture_clear( + &fixture + ); +} + +/** + * @brief Corrompt volontairement la destination avant son contrôle. + */ +static void test_evidence_copy_corrupt_before_verify( + const char *destination_path, + gpointer user_data +) +{ + TestEvidenceCopyVerifyFailureContext *context = + user_data; + + GError *error = NULL; + + assert(context != NULL); + assert(destination_path != NULL); + + context->call_count++; + + assert( + g_file_set_contents( + destination_path, + "copie corrompue", + 15, + &error + ) + ); + + assert(error == NULL); +} + +/** + * @brief Vérifie le nettoyage après une corruption de la copie. + */ +static void test_evidence_copy_verification_failure_cleanup(void) +{ + TestEvidenceCopyFixture fixture = + test_evidence_copy_fixture_create(); + + TestEvidenceCopyVerifyFailureContext context = {0}; + + EvidenceCopyResult *result = NULL; + + char *source_path = NULL; + char *destination_path = NULL; + char *source_contents = NULL; + + gsize source_contents_size = 0; + + GError *error = NULL; + + source_path = + test_evidence_copy_build_source_path( + &fixture, + "verify-source.bin" + ); + + destination_path = + test_evidence_copy_build_destination_path( + &fixture, + "verify-copy.bin" + ); + + assert(source_path != NULL); + assert(destination_path != NULL); + + assert( + g_file_set_contents( + source_path, + "preuve originale", + 16, + &error + ) + ); + + assert(error == NULL); + + evidence_copy_test_set_before_verify_hook( + test_evidence_copy_corrupt_before_verify, + &context + ); + + result = + evidence_copy_file( + source_path, + fixture.destination_directory, + "verify-copy.bin", + NULL, + &error + ); + + evidence_copy_test_set_before_verify_hook( + NULL, + NULL + ); + + assert(result == NULL); + assert(context.call_count == 1); + + test_evidence_copy_assert_error( + error, + EVIDENCE_COPY_ERROR_VERIFY + ); + + g_clear_error( + &error + ); + + /* + * La copie corrompue doit avoir été supprimée. + */ + test_evidence_copy_assert_destination_absent( + destination_path + ); + + /* + * La source doit être restée intacte. + */ + assert( + g_file_get_contents( + source_path, + &source_contents, + &source_contents_size, + &error + ) + ); + + assert(error == NULL); + assert(source_contents_size == 16); + + assert( + memcmp( + source_contents, + "preuve originale", + 16 + ) == 0 + ); + + g_free( + source_contents + ); + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + destination_path + ); + + g_free( + source_path + ); + + test_evidence_copy_fixture_clear( + &fixture + ); +} + +/** + * @brief Modifie la source après sa copie. + */ +static void test_evidence_copy_modify_source_before_verify( + const char *destination_path, + gpointer user_data +) +{ + TestEvidenceCopySourceModificationContext *context = + user_data; + + GError *error = NULL; + + assert(destination_path != NULL); + assert(context != NULL); + assert(context->source_path != NULL); + + context->call_count++; + + assert( + g_file_set_contents( + context->source_path, + "source modifiee", + 15, + &error + ) + ); + + assert(error == NULL); +} +/** + * @brief Vérifie la détection d’une source modifiée pendant la copie. + */ +static void test_evidence_copy_source_modified_during_copy(void) +{ + TestEvidenceCopyFixture fixture = + test_evidence_copy_fixture_create(); + + TestEvidenceCopySourceModificationContext context = {0}; + + EvidenceCopyResult *result = NULL; + + char *source_path = NULL; + char *destination_path = NULL; + char *source_contents = NULL; + + gsize source_contents_size = 0; + + GError *error = NULL; + + source_path = + test_evidence_copy_build_source_path( + &fixture, + "changing-source.bin" + ); + + destination_path = + test_evidence_copy_build_destination_path( + &fixture, + "changing-copy.bin" + ); + + assert(source_path != NULL); + assert(destination_path != NULL); + + assert( + g_file_set_contents( + source_path, + "source initiale", + 15, + &error + ) + ); + + assert(error == NULL); + + context.source_path = + source_path; + + evidence_copy_test_set_before_verify_hook( + test_evidence_copy_modify_source_before_verify, + &context + ); + + result = + evidence_copy_file( + source_path, + fixture.destination_directory, + "changing-copy.bin", + NULL, + &error + ); + + evidence_copy_test_set_before_verify_hook( + NULL, + NULL + ); + + assert(result == NULL); + assert(context.call_count == 1); + + test_evidence_copy_assert_error( + error, + EVIDENCE_COPY_ERROR_VERIFY + ); + + g_clear_error( + &error + ); + + /* + * La destination n’est pas fiable et doit être supprimée. + */ + test_evidence_copy_assert_destination_absent( + destination_path + ); + + /* + * EvidenceCopy ne doit pas tenter de restaurer ou modifier la source. + * Elle reste donc dans l’état imposé par le test. + */ + assert( + g_file_get_contents( + source_path, + &source_contents, + &source_contents_size, + &error + ) + ); + + assert(error == NULL); + assert(source_contents_size == 15); + + assert( + memcmp( + source_contents, + "source modifiee", + 15 + ) == 0 + ); + + g_free( + source_contents + ); + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + destination_path + ); + + g_free( + source_path + ); + + test_evidence_copy_fixture_clear( + &fixture + ); +} + +/** + * @brief Simule systématiquement un échec de suppression. + */ +static gboolean test_evidence_copy_fail_cleanup( + const char *destination_path, + gpointer user_data +) +{ + TestEvidenceCopyCleanupFailureContext *context = + user_data; + + assert(context != NULL); + assert(destination_path != NULL); + assert(destination_path[0] != '\0'); + + context->call_count++; + + return TRUE; +} + +/** + * @brief Vérifie le signalement d’un fichier orphelin. + */ +static void test_evidence_copy_cleanup_failure_reported(void) +{ + TestEvidenceCopyFixture fixture = + test_evidence_copy_fixture_create(); + + TestEvidenceCopyVerifyFailureContext verify_context = {0}; + TestEvidenceCopyCleanupFailureContext cleanup_context = {0}; + + EvidenceCopyResult *result = NULL; + + char *source_path = NULL; + char *destination_path = NULL; + + GError *error = NULL; + + source_path = + test_evidence_copy_build_source_path( + &fixture, + "cleanup-source.bin" + ); + + destination_path = + test_evidence_copy_build_destination_path( + &fixture, + "cleanup-copy.bin" + ); + + assert(source_path != NULL); + assert(destination_path != NULL); + + assert( + g_file_set_contents( + source_path, + "preuve originale", + 16, + &error + ) + ); + + assert(error == NULL); + + evidence_copy_test_set_before_verify_hook( + test_evidence_copy_corrupt_before_verify, + &verify_context + ); + + evidence_copy_test_set_cleanup_hook( + test_evidence_copy_fail_cleanup, + &cleanup_context + ); + + result = + evidence_copy_file( + source_path, + fixture.destination_directory, + "cleanup-copy.bin", + NULL, + &error + ); + + /* + * Retirer les deux crochets avant toute autre opération. + */ + evidence_copy_test_set_cleanup_hook( + NULL, + NULL + ); + + evidence_copy_test_set_before_verify_hook( + NULL, + NULL + ); + + assert(result == NULL); + assert(verify_context.call_count == 1); + assert(cleanup_context.call_count == 1); + + test_evidence_copy_assert_error( + error, + EVIDENCE_COPY_ERROR_CLEANUP + ); + + /* + * Le message doit identifier le fichier potentiellement orphelin. + */ + assert( + strstr( + error->message, + destination_path + ) != NULL + ); + + /* + * La cause initiale doit rester visible. + */ + assert( + strstr( + error->message, + "Cause initiale" + ) != NULL + ); + + g_clear_error( + &error + ); + + /* + * L’échec étant simulé, le fichier doit toujours exister. + */ + assert( + g_file_test( + destination_path, + G_FILE_TEST_IS_REGULAR + ) + ); + + /* + * Le test effectue ensuite lui-même le nettoyage réel. + */ + assert( + g_remove( + destination_path + ) == 0 + ); + + assert( + g_remove( + source_path + ) == 0 + ); + + g_free( + destination_path + ); + + g_free( + source_path + ); + + test_evidence_copy_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie l’indépendance de plusieurs copies successives. + */ +static void test_evidence_copy_successive_calls(void) +{ + TestEvidenceCopyFixture fixture = + test_evidence_copy_fixture_create(); + + EvidenceCopyResult *first_result = NULL; + EvidenceCopyResult *second_result = NULL; + + char *first_source_path = NULL; + char *second_source_path = NULL; + + char *first_destination_path = NULL; + char *second_destination_path = NULL; + + GError *error = NULL; + + first_source_path = + test_evidence_copy_build_source_path( + &fixture, + "first-source.bin" + ); + + second_source_path = + test_evidence_copy_build_source_path( + &fixture, + "second-source.bin" + ); + + first_destination_path = + test_evidence_copy_build_destination_path( + &fixture, + "first-copy.bin" + ); + + second_destination_path = + test_evidence_copy_build_destination_path( + &fixture, + "second-copy.bin" + ); + + assert(first_source_path != NULL); + assert(second_source_path != NULL); + assert(first_destination_path != NULL); + assert(second_destination_path != NULL); + + assert( + g_file_set_contents( + first_source_path, + "premiere preuve", + 15, + &error + ) + ); + + assert(error == NULL); + + assert( + g_file_set_contents( + second_source_path, + "deuxieme preuve", + 15, + &error + ) + ); + + assert(error == NULL); + + first_result = + evidence_copy_file( + first_source_path, + fixture.destination_directory, + "first-copy.bin", + NULL, + &error + ); + + assert(error == NULL); + assert(first_result != NULL); + + second_result = + evidence_copy_file( + second_source_path, + fixture.destination_directory, + "second-copy.bin", + NULL, + &error + ); + + assert(error == NULL); + assert(second_result != NULL); + + assert( + strcmp( + evidence_copy_result_get_destination_path( + first_result + ), + first_destination_path + ) == 0 + ); + + assert( + strcmp( + evidence_copy_result_get_destination_path( + second_result + ), + second_destination_path + ) == 0 + ); + + assert( + strcmp( + evidence_copy_result_get_destination_name( + first_result + ), + "first-copy.bin" + ) == 0 + ); + + assert( + strcmp( + evidence_copy_result_get_destination_name( + second_result + ), + "second-copy.bin" + ) == 0 + ); + + assert( + evidence_copy_result_get_size_bytes( + first_result + ) == 15 + ); + + assert( + evidence_copy_result_get_size_bytes( + second_result + ) == 15 + ); + + assert( + strcmp( + evidence_copy_result_get_sha256( + first_result + ), + evidence_copy_result_get_sha256( + second_result + ) + ) != 0 + ); + + assert( + g_file_test( + first_destination_path, + G_FILE_TEST_IS_REGULAR + ) + ); + + assert( + g_file_test( + second_destination_path, + G_FILE_TEST_IS_REGULAR + ) + ); + + evidence_copy_result_free( + second_result + ); + + evidence_copy_result_free( + first_result + ); + + assert( + g_remove( + second_destination_path + ) == 0 + ); + + assert( + g_remove( + first_destination_path + ) == 0 + ); + + assert( + g_remove( + second_source_path + ) == 0 + ); + + assert( + g_remove( + first_source_path + ) == 0 + ); + + g_free( + second_destination_path + ); + + g_free( + first_destination_path + ); + + g_free( + second_source_path + ); + + g_free( + first_source_path + ); + + test_evidence_copy_fixture_clear( + &fixture + ); +} + +int main(void) +{ + test_evidence_copy_null_result(); + test_evidence_copy_text_file(); + test_evidence_copy_empty_file(); + test_evidence_copy_binary_file(); + test_evidence_copy_multiple_blocks(); + test_evidence_copy_permissions(); + test_evidence_copy_destination_exists(); + test_evidence_copy_invalid_arguments(); + test_evidence_copy_invalid_destination_names(); + test_evidence_copy_missing_source(); + test_evidence_copy_source_directory(); + test_evidence_copy_source_symbolic_link(); + test_evidence_copy_source_named_pipe(); + test_evidence_copy_cancelled_before_start(); + test_evidence_copy_missing_destination_directory(); + test_evidence_copy_destination_is_file(); + test_evidence_copy_destination_symbolic_link(); + test_evidence_copy_cancelled_during_copy(); + test_evidence_copy_write_failure_cleanup(); + test_evidence_copy_verification_failure_cleanup(); + test_evidence_copy_source_modified_during_copy(); + test_evidence_copy_cleanup_failure_reported(); + test_evidence_copy_successive_calls(); + + printf( + "EvidenceCopy : premiers tests de copie valides.\n" + ); + + return 0; +}