diff --git a/Makefile b/Makefile index 809146d..a918eac 100644 --- a/Makefile +++ b/Makefile @@ -52,6 +52,7 @@ TEST_TOOL_PROCESS := tests/test_tool_process 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 all: $(TARGET) @@ -212,6 +213,13 @@ $(TEST_TOOL_INITIALIZER): \ src/core/tool_process.c $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) +$(TEST_FILE_HASH): \ + tests/test_file_hash.c \ + src/core/file_hash.c + $(CC) $(TEST_CFLAGS) \ + -DFILE_HASH_ENABLE_TEST_HOOKS \ + $^ -o $@ $(TEST_LDFLAGS) + test: \ $(TEST_NODE) \ $(TEST_TREE_MODEL) \ @@ -232,7 +240,8 @@ test: \ $(TEST_TOOL_PROCESS) \ $(TEST_TOOL_TASK) \ $(TEST_TOOL_CATALOG) \ - $(TEST_TOOL_INITIALIZER) + $(TEST_TOOL_INITIALIZER) \ + $(TEST_FILE_HASH) @echo "Exécution des tests..." @./$(TEST_NODE) @./$(TEST_TREE_MODEL) @@ -254,6 +263,7 @@ test: \ @$(TEST_TOOL_TASK) @$(TEST_TOOL_CATALOG) @$(TEST_TOOL_INITIALIZER) + @$(TEST_FILE_HASH) @echo "Tous les tests sont valides." %.o: %.c @@ -283,6 +293,8 @@ clean: $(TEST_TOOL_PROCESS) \ $(TEST_TOOL_TASK) \ $(TEST_TOOL_CATALOG) \ - $(TEST_TOOL_INITIALIZER) + $(TEST_TOOL_INITIALIZER) \ + $(TEST_FILE_HASH) + .PHONY: clean run test diff --git a/include/core/file_hash.h b/include/core/file_hash.h new file mode 100644 index 0000000..b430b8b --- /dev/null +++ b/include/core/file_hash.h @@ -0,0 +1,99 @@ +/****************************************************************************** + * @file file_hash.h + * @brief Calcul des empreintes cryptographiques des fichiers de preuve. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_FILE_HASH_H +#define LABFY_INVESTIGATION_FILE_HASH_H + +#include +#include + +/** + * @brief Catégories d’erreurs du module FileHash. + */ +typedef enum +{ + FILE_HASH_ERROR_INVALID_ARGUMENT, + FILE_HASH_ERROR_NOT_FOUND, + FILE_HASH_ERROR_NOT_REGULAR, + FILE_HASH_ERROR_OPEN, + FILE_HASH_ERROR_READ, + FILE_HASH_ERROR_CANCELLED, + FILE_HASH_ERROR_MEMORY +} FileHashError; + +/** + * @brief Domaine d’erreurs du module FileHash. + */ +#define FILE_HASH_ERROR \ + file_hash_error_quark() + +/** + * @brief Retourne le domaine d’erreurs du module FileHash. + */ +GQuark file_hash_error_quark(void); + +/** + * @brief Calcule le SHA-256 et la taille d’un fichier régulier. + * + * Le fichier est lu progressivement et n’est jamais chargé intégralement + * en mémoire. + * + * Le chemin final ne doit pas être un lien symbolique. + * + * Avant l’appel : + * + * - out_sha256 doit être valide ; + * - *out_sha256 doit valoir NULL ; + * - out_size_bytes doit être valide ; + * - error doit être NULL ou pointer vers NULL. + * + * Après un succès : + * + * - *out_sha256 contient une chaîne de 64 caractères hexadécimaux + * minuscules, à libérer avec g_free() ; + * - *out_size_bytes contient le nombre d’octets effectivement lus. + * + * Après un échec : + * + * - *out_sha256 reste NULL ; + * - *out_size_bytes vaut 0. + * + * @param file_path Chemin du fichier à analyser. + * @param cancellable Objet d’annulation facultatif. + * @param out_sha256 Destination de la chaîne SHA-256. + * @param out_size_bytes Destination de la taille du fichier. + * @param error Adresse recevant une éventuelle erreur. + * + * @return TRUE en cas de succès, sinon FALSE. + */ +gboolean file_hash_compute_sha256( + const char *file_path, + GCancellable *cancellable, + char **out_sha256, + guint64 *out_size_bytes, + GError **error +); + +#ifdef FILE_HASH_ENABLE_TEST_HOOKS + +/** + * @brief Crochet appelé après chaque bloc lu, réservé aux tests. + */ +typedef void (*FileHashTestBlockHook)( + guint64 total_size_bytes, + gpointer user_data +); + +/** + * @brief Configure le crochet de lecture réservé aux tests. + */ +void file_hash_test_set_block_hook( + FileHashTestBlockHook block_hook, + gpointer user_data +); + +#endif + +#endif diff --git a/labfy-investigation b/labfy-investigation index 18572e4..f806d19 100755 Binary files a/labfy-investigation and b/labfy-investigation differ diff --git a/src/core/file_hash.c b/src/core/file_hash.c new file mode 100644 index 0000000..e7b1576 --- /dev/null +++ b/src/core/file_hash.c @@ -0,0 +1,553 @@ +/****************************************************************************** + * @file file_hash.c + * @brief Calcul progressif du SHA-256 des fichiers de preuve. + ******************************************************************************/ + +#define _POSIX_C_SOURCE 200809L + +#include "core/file_hash.h" + +#include +#include +#include +#include + +/** + * @brief Taille du tampon de lecture. + */ +#define FILE_HASH_BLOCK_SIZE (64U * 1024U) + +/** + * @brief Nombre d’octets composant un SHA-256. + */ +#define FILE_HASH_SHA256_DIGEST_SIZE 32U + +/** + * @brief Nombre de caractères hexadécimaux composant un SHA-256. + */ +#define FILE_HASH_SHA256_TEXT_SIZE 64U + +#ifdef FILE_HASH_ENABLE_TEST_HOOKS + +static FileHashTestBlockHook file_hash_test_block_hook = NULL; +static gpointer file_hash_test_block_hook_data = NULL; + +void file_hash_test_set_block_hook( + FileHashTestBlockHook block_hook, + gpointer user_data +) +{ + file_hash_test_block_hook = block_hook; + file_hash_test_block_hook_data = user_data; +} + +static void file_hash_test_notify_block( + guint64 total_size_bytes +) +{ + if (file_hash_test_block_hook == NULL) + { + return; + } + + file_hash_test_block_hook( + total_size_bytes, + file_hash_test_block_hook_data + ); +} + +#else + +static void file_hash_test_notify_block( + guint64 total_size_bytes +) +{ + /* + * Le paramètre est volontairement inutilisé dans la version + * de production. + */ + (void) total_size_bytes; +} + +#endif + +/** + * @brief Enregistre une erreur littérale. + */ +static void file_hash_set_error_literal( + GError **error, + FileHashError error_code, + const char *error_message +) +{ + if (error == NULL) + { + return; + } + + g_set_error_literal( + error, + FILE_HASH_ERROR, + error_code, + error_message + ); +} + +/** + * @brief Enregistre une erreur contenant un chemin et une erreur système. + */ +static void file_hash_set_system_error( + GError **error, + FileHashError error_code, + const char *context, + const char *file_path, + int system_error +) +{ + if (error == NULL) + { + return; + } + + g_set_error( + error, + FILE_HASH_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 file_hash_check_cancelled( + GCancellable *cancellable, + GError **error +) +{ + if (cancellable == NULL || + !g_cancellable_is_cancelled(cancellable)) + { + return FALSE; + } + + file_hash_set_error_literal( + error, + FILE_HASH_ERROR_CANCELLED, + "Le calcul du SHA-256 a été annulé." + ); + + return TRUE; +} + +/** + * @brief Convertit le condensat SHA-256 en texte hexadécimal minuscule. + */ +static char *file_hash_digest_to_hex( + const guint8 *digest, + gsize digest_size, + GError **error +) +{ + static const char hexadecimal_digits[] = + "0123456789abcdef"; + + char *sha256 = NULL; + gsize digest_index = 0; + + if (digest == NULL || + digest_size != FILE_HASH_SHA256_DIGEST_SIZE) + { + file_hash_set_error_literal( + error, + FILE_HASH_ERROR_INVALID_ARGUMENT, + "Le condensat SHA-256 est invalide." + ); + + return NULL; + } + + sha256 = g_try_malloc0( + FILE_HASH_SHA256_TEXT_SIZE + 1U + ); + + if (sha256 == NULL) + { + file_hash_set_error_literal( + error, + FILE_HASH_ERROR_MEMORY, + "Impossible d’allouer la chaîne SHA-256." + ); + + return NULL; + } + + for (digest_index = 0; + digest_index < digest_size; + digest_index++) + { + sha256[digest_index * 2U] = + hexadecimal_digits[ + digest[digest_index] >> 4U + ]; + + sha256[digest_index * 2U + 1U] = + hexadecimal_digits[ + digest[digest_index] & 0x0FU + ]; + } + + return sha256; +} + +GQuark file_hash_error_quark(void) +{ + return g_quark_from_static_string( + "file-hash-error-quark" + ); +} + +gboolean file_hash_compute_sha256( + const char *file_path, + GCancellable *cancellable, + char **out_sha256, + guint64 *out_size_bytes, + GError **error +) +{ + GChecksum *checksum = NULL; + + guint8 *read_buffer = NULL; + guint8 digest[FILE_HASH_SHA256_DIGEST_SIZE]; + + struct stat file_status; + + char *sha256 = NULL; + + gsize digest_size = + FILE_HASH_SHA256_DIGEST_SIZE; + + guint64 total_size_bytes = 0; + + ssize_t read_size = 0; + + int file_descriptor = -1; + int saved_errno = 0; + + gboolean success = FALSE; + + g_return_val_if_fail( + error == NULL || *error == NULL, + FALSE + ); + + if (out_size_bytes != NULL) + { + *out_size_bytes = 0; + } + + if (file_path == NULL || + file_path[0] == '\0' || + out_sha256 == NULL || + out_size_bytes == NULL) + { + file_hash_set_error_literal( + error, + FILE_HASH_ERROR_INVALID_ARGUMENT, + "Les paramètres du calcul SHA-256 sont invalides." + ); + + return FALSE; + } + + /* + * Le module ne doit jamais libérer une ancienne chaîne appartenant + * à l’appelant. + */ + if (*out_sha256 != NULL) + { + file_hash_set_error_literal( + error, + FILE_HASH_ERROR_INVALID_ARGUMENT, + "La destination SHA-256 doit initialement valoir NULL." + ); + + return FALSE; + } + + if (file_hash_check_cancelled( + cancellable, + error + )) + { + return FALSE; + } + + /* + * O_NOFOLLOW refuse un lien symbolique utilisé comme dernier composant. + * + * O_NONBLOCK évite de rester bloqué lors de l’ouverture accidentelle + * d’un tube nommé ou d’un fichier spécial. + */ + file_descriptor = open( + file_path, + O_RDONLY | + O_CLOEXEC | + O_NOFOLLOW | + O_NONBLOCK + ); + + if (file_descriptor < 0) + { + saved_errno = errno; + + if (saved_errno == ENOENT || + saved_errno == ENOTDIR) + { + file_hash_set_system_error( + error, + FILE_HASH_ERROR_NOT_FOUND, + "Le fichier est introuvable", + file_path, + saved_errno + ); + } + else if (saved_errno == ELOOP) + { + file_hash_set_error_literal( + error, + FILE_HASH_ERROR_NOT_REGULAR, + "Le chemin désigne un lien symbolique." + ); + } + else + { + file_hash_set_system_error( + error, + FILE_HASH_ERROR_OPEN, + "Impossible d’ouvrir le fichier", + file_path, + saved_errno + ); + } + + goto cleanup; + } + + if (fstat( + file_descriptor, + &file_status + ) != 0) + { + saved_errno = errno; + + file_hash_set_system_error( + error, + FILE_HASH_ERROR_OPEN, + "Impossible d’inspecter le fichier", + file_path, + saved_errno + ); + + goto cleanup; + } + + if (!S_ISREG(file_status.st_mode)) + { + file_hash_set_error_literal( + error, + FILE_HASH_ERROR_NOT_REGULAR, + "Le chemin ne désigne pas un fichier régulier." + ); + + goto cleanup; + } + + if (file_hash_check_cancelled( + cancellable, + error + )) + { + goto cleanup; + } + + checksum = g_checksum_new( + G_CHECKSUM_SHA256 + ); + + if (checksum == NULL) + { + file_hash_set_error_literal( + error, + FILE_HASH_ERROR_MEMORY, + "Impossible d’allouer le calculateur SHA-256." + ); + + goto cleanup; + } + + read_buffer = g_try_malloc( + FILE_HASH_BLOCK_SIZE + ); + + if (read_buffer == NULL) + { + file_hash_set_error_literal( + error, + FILE_HASH_ERROR_MEMORY, + "Impossible d’allouer le tampon de lecture." + ); + + goto cleanup; + } + + while (TRUE) + { + if (file_hash_check_cancelled( + cancellable, + error + )) + { + goto cleanup; + } + + read_size = read( + file_descriptor, + read_buffer, + FILE_HASH_BLOCK_SIZE + ); + + if (read_size < 0) + { + saved_errno = errno; + + if (saved_errno == EINTR) + { + continue; + } + + file_hash_set_system_error( + error, + FILE_HASH_ERROR_READ, + "Impossible de lire le fichier", + file_path, + saved_errno + ); + + goto cleanup; + } + + if (read_size == 0) + { + break; + } + + g_checksum_update( + checksum, + read_buffer, + (gssize) read_size + ); + + total_size_bytes += + (guint64) read_size; + + file_hash_test_notify_block( + total_size_bytes + ); + + if (file_hash_check_cancelled( + cancellable, + error + )) + { + goto cleanup; + } + } + + if (file_hash_check_cancelled( + cancellable, + error + )) + { + goto cleanup; + } + + g_checksum_get_digest( + checksum, + digest, + &digest_size + ); + + if (digest_size != + FILE_HASH_SHA256_DIGEST_SIZE) + { + file_hash_set_error_literal( + error, + FILE_HASH_ERROR_READ, + "La taille du condensat SHA-256 est invalide." + ); + + goto cleanup; + } + + sha256 = file_hash_digest_to_hex( + digest, + digest_size, + error + ); + + if (sha256 == NULL) + { + goto cleanup; + } + + /* + * Les sorties ne sont publiées qu’après le succès complet. + */ + *out_sha256 = sha256; + *out_size_bytes = total_size_bytes; + + sha256 = NULL; + success = TRUE; + +cleanup: + + g_free( + sha256 + ); + + g_free( + read_buffer + ); + + if (checksum != NULL) + { + g_checksum_free( + checksum + ); + } + + if (file_descriptor >= 0) + { + if (close( + file_descriptor + ) != 0 && + success) + { + /* + * Une erreur de fermeture en lecture seule ne modifie pas + * le condensat déjà calculé. Elle est néanmoins signalée. + */ + g_warning( + "Impossible de fermer proprement le fichier '%s' : %s", + file_path, + g_strerror(errno) + ); + } + } + + return success; +} diff --git a/tests/test_file_hash b/tests/test_file_hash new file mode 100755 index 0000000..27ec998 Binary files /dev/null and b/tests/test_file_hash differ diff --git a/tests/test_file_hash.c b/tests/test_file_hash.c new file mode 100644 index 0000000..49c1d96 --- /dev/null +++ b/tests/test_file_hash.c @@ -0,0 +1,1355 @@ +/****************************************************************************** + * @file test_file_hash.c + * @brief Tests du calcul SHA-256 des fichiers de preuve. + ******************************************************************************/ + +#define _POSIX_C_SOURCE 200809L + +#ifndef FILE_HASH_ENABLE_TEST_HOOKS +#define FILE_HASH_ENABLE_TEST_HOOKS +#endif + +#include "core/file_hash.h" + +#include +#include +#include +#include + +#include +#include + +#include + +/** + * @brief Environnement temporaire des tests FileHash. + */ +typedef struct +{ + char *temporary_directory; +} TestFileHashFixture; + +/** + * @brief Contexte d’annulation déclenchée après un bloc. + */ +typedef struct +{ + GCancellable *cancellable; + guint block_count; +} TestFileHashCancellationContext; + +/** + * @brief Vérifie une erreur FileHash. + */ +static void test_file_hash_assert_error( + const GError *error, + FileHashError expected_error +) +{ + assert(error != NULL); + assert(error->domain == FILE_HASH_ERROR); + + assert( + error->code == + (gint) expected_error + ); + + assert(error->message != NULL); + assert(error->message[0] != '\0'); +} + +/** + * @brief Crée un dossier temporaire isolé. + */ +static TestFileHashFixture test_file_hash_fixture_create(void) +{ + TestFileHashFixture fixture = {0}; + GError *error = NULL; + + fixture.temporary_directory = + g_dir_make_tmp( + "labfy-file-hash-test-XXXXXX", + &error + ); + + assert(fixture.temporary_directory != NULL); + assert(error == NULL); + + return fixture; +} + +/** + * @brief Supprime le dossier temporaire. + * + * Tous les fichiers qu’il contient doivent avoir été supprimés avant. + */ +static void test_file_hash_fixture_clear( + TestFileHashFixture *fixture +) +{ + assert(fixture != NULL); + assert(fixture->temporary_directory != NULL); + + assert( + g_rmdir( + fixture->temporary_directory + ) == 0 + ); + + g_free( + fixture->temporary_directory + ); + + fixture->temporary_directory = NULL; +} + +/** + * @brief Construit le chemin d’un fichier dans la fixture. + */ +static char *test_file_hash_build_path( + const TestFileHashFixture *fixture, + const char *file_name +) +{ + assert(fixture != NULL); + assert(fixture->temporary_directory != NULL); + assert(file_name != NULL); + + return g_build_filename( + fixture->temporary_directory, + file_name, + NULL + ); +} + +/** + * @brief Vérifie le SHA-256 d’un fichier vide. + */ +static void test_file_hash_empty_file(void) +{ + TestFileHashFixture fixture = + test_file_hash_fixture_create(); + + char *file_path = NULL; + char *sha256 = NULL; + + guint64 size_bytes = 42; + + GError *error = NULL; + + file_path = + test_file_hash_build_path( + &fixture, + "empty.bin" + ); + + assert(file_path != NULL); + + assert( + g_file_set_contents( + file_path, + "", + 0, + &error + ) + ); + + assert(error == NULL); + + assert( + file_hash_compute_sha256( + file_path, + NULL, + &sha256, + &size_bytes, + &error + ) + ); + + assert(error == NULL); + assert(sha256 != NULL); + assert(size_bytes == 0); + + assert( + strlen( + sha256 + ) == 64 + ); + + assert( + strcmp( + sha256, + "e3b0c44298fc1c149afbf4c8996fb924" + "27ae41e4649b934ca495991b7852b855" + ) == 0 + ); + + g_free( + sha256 + ); + + assert( + g_remove( + file_path + ) == 0 + ); + + g_free( + file_path + ); + + test_file_hash_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le SHA-256 du contenu connu « abc ». + */ +static void test_file_hash_known_content(void) +{ + TestFileHashFixture fixture = + test_file_hash_fixture_create(); + + char *file_path = NULL; + char *sha256 = NULL; + + guint64 size_bytes = 0; + + GError *error = NULL; + + file_path = + test_file_hash_build_path( + &fixture, + "abc.txt" + ); + + assert(file_path != NULL); + + /* + * Aucun saut de ligne ne doit être ajouté. + */ + assert( + g_file_set_contents( + file_path, + "abc", + 3, + &error + ) + ); + + assert(error == NULL); + + assert( + file_hash_compute_sha256( + file_path, + NULL, + &sha256, + &size_bytes, + &error + ) + ); + + assert(error == NULL); + assert(sha256 != NULL); + assert(size_bytes == 3); + + assert( + strlen( + sha256 + ) == 64 + ); + + assert( + strcmp( + sha256, + "ba7816bf8f01cfea414140de5dae2223" + "b00361a396177a9cb410ff61f20015ad" + ) == 0 + ); + + g_free( + sha256 + ); + + assert( + g_remove( + file_path + ) == 0 + ); + + g_free( + file_path + ); + + test_file_hash_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le calcul d’un fichier réparti sur plusieurs blocs. + */ +static void test_file_hash_multiple_blocks(void) +{ + TestFileHashFixture fixture = + test_file_hash_fixture_create(); + + const gsize data_size = + (200U * 1024U) + 123U; + + guint8 *data = NULL; + + char *file_path = NULL; + char *sha256 = NULL; + + guint64 size_bytes = 0; + gsize data_index = 0; + + GError *error = NULL; + + file_path = + test_file_hash_build_path( + &fixture, + "multiple_blocks.bin" + ); + + assert(file_path != NULL); + + data = g_malloc( + data_size + ); + + assert(data != NULL); + + /* + * Motif déterministe couvrant toutes les limites de blocs. + */ + for (data_index = 0; + data_index < data_size; + data_index++) + { + data[data_index] = + (guint8) (data_index % 251U); + } + + assert( + g_file_set_contents( + file_path, + (const char *) data, + (gssize) data_size, + &error + ) + ); + + assert(error == NULL); + + assert( + file_hash_compute_sha256( + file_path, + NULL, + &sha256, + &size_bytes, + &error + ) + ); + + assert(error == NULL); + assert(sha256 != NULL); + + assert( + size_bytes == + (guint64) data_size + ); + + assert( + strcmp( + sha256, + "bc5ad58e926b1f79d9840418910f47ba" + "1e947fb137170b8d5403a537b888c4af" + ) == 0 + ); + + g_free( + sha256 + ); + + g_free( + data + ); + + assert( + g_remove( + file_path + ) == 0 + ); + + g_free( + file_path + ); + + test_file_hash_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie que les données sont traitées comme des octets. + */ +static void test_file_hash_binary_content(void) +{ + TestFileHashFixture fixture = + test_file_hash_fixture_create(); + + static const guint8 binary_data[] = + { + 0x00, + 0xFF, + 0x80, + 0x41, + 0x00, + 0x7F, + 0x10 + }; + + char *file_path = NULL; + char *sha256 = NULL; + + guint64 size_bytes = 0; + + GError *error = NULL; + + file_path = + test_file_hash_build_path( + &fixture, + "binary.bin" + ); + + assert(file_path != NULL); + + assert( + g_file_set_contents( + file_path, + (const char *) binary_data, + (gssize) sizeof(binary_data), + &error + ) + ); + + assert(error == NULL); + + assert( + file_hash_compute_sha256( + file_path, + NULL, + &sha256, + &size_bytes, + &error + ) + ); + + assert(error == NULL); + assert(sha256 != NULL); + + assert( + size_bytes == + sizeof(binary_data) + ); + + assert( + strcmp( + sha256, + "c90d3a7754f84abc867e780e5a0c0f8d" + "a5c663bd1766ad89e538536a3763f367" + ) == 0 + ); + + g_free( + sha256 + ); + + assert( + g_remove( + file_path + ) == 0 + ); + + g_free( + file_path + ); + + test_file_hash_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie le refus des paramètres invalides. + */ +static void test_file_hash_invalid_arguments(void) +{ + char *sha256 = NULL; + guint64 size_bytes = 123; + + GError *error = NULL; + + assert( + !file_hash_compute_sha256( + NULL, + NULL, + &sha256, + &size_bytes, + &error + ) + ); + + assert(sha256 == NULL); + assert(size_bytes == 0); + + test_file_hash_assert_error( + error, + FILE_HASH_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + size_bytes = 123; + + assert( + !file_hash_compute_sha256( + "", + NULL, + &sha256, + &size_bytes, + &error + ) + ); + + assert(sha256 == NULL); + assert(size_bytes == 0); + + test_file_hash_assert_error( + error, + FILE_HASH_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + size_bytes = 123; + + assert( + !file_hash_compute_sha256( + "/tmp/file-hash-invalid", + NULL, + NULL, + &size_bytes, + &error + ) + ); + + assert(size_bytes == 0); + + test_file_hash_assert_error( + error, + FILE_HASH_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + assert( + !file_hash_compute_sha256( + "/tmp/file-hash-invalid", + NULL, + &sha256, + NULL, + &error + ) + ); + + assert(sha256 == NULL); + + test_file_hash_assert_error( + error, + FILE_HASH_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); +} + +/** + * @brief Vérifie que le module ne remplace pas une chaîne appartenant + * déjà à l’appelant. + */ +static void test_file_hash_non_null_output(void) +{ + char *sha256 = + g_strdup( + "ancienne-valeur" + ); + + guint64 size_bytes = 123; + + GError *error = NULL; + + assert(sha256 != NULL); + + assert( + !file_hash_compute_sha256( + "/tmp/file-hash-output-test", + NULL, + &sha256, + &size_bytes, + &error + ) + ); + + /* + * La chaîne reste la propriété de l’appelant et n’est pas modifiée. + */ + assert( + strcmp( + sha256, + "ancienne-valeur" + ) == 0 + ); + + assert(size_bytes == 0); + + test_file_hash_assert_error( + error, + FILE_HASH_ERROR_INVALID_ARGUMENT + ); + + g_clear_error( + &error + ); + + g_free( + sha256 + ); +} + +/** + * @brief Vérifie l’erreur produite pour un chemin absent. + */ +static void test_file_hash_missing_file(void) +{ + TestFileHashFixture fixture = + test_file_hash_fixture_create(); + + char *file_path = NULL; + char *sha256 = NULL; + + guint64 size_bytes = 123; + + GError *error = NULL; + + file_path = + test_file_hash_build_path( + &fixture, + "absent.bin" + ); + + assert(file_path != NULL); + + assert( + !file_hash_compute_sha256( + file_path, + NULL, + &sha256, + &size_bytes, + &error + ) + ); + + assert(sha256 == NULL); + assert(size_bytes == 0); + + test_file_hash_assert_error( + error, + FILE_HASH_ERROR_NOT_FOUND + ); + + g_clear_error( + &error + ); + + g_free( + file_path + ); + + test_file_hash_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie qu’un dossier est refusé. + */ +static void test_file_hash_directory(void) +{ + TestFileHashFixture fixture = + test_file_hash_fixture_create(); + + char *sha256 = NULL; + guint64 size_bytes = 123; + + GError *error = NULL; + + assert( + !file_hash_compute_sha256( + fixture.temporary_directory, + NULL, + &sha256, + &size_bytes, + &error + ) + ); + + assert(sha256 == NULL); + assert(size_bytes == 0); + + test_file_hash_assert_error( + error, + FILE_HASH_ERROR_NOT_REGULAR + ); + + g_clear_error( + &error + ); + + test_file_hash_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie qu’un lien symbolique n’est jamais suivi. + */ +static void test_file_hash_symbolic_link(void) +{ + TestFileHashFixture fixture = + test_file_hash_fixture_create(); + + char *target_path = NULL; + char *link_path = NULL; + char *sha256 = NULL; + + guint64 size_bytes = 123; + + GError *error = NULL; + + target_path = + test_file_hash_build_path( + &fixture, + "target.bin" + ); + + link_path = + test_file_hash_build_path( + &fixture, + "link.bin" + ); + + assert(target_path != NULL); + assert(link_path != NULL); + + assert( + g_file_set_contents( + target_path, + "contenu", + 7, + &error + ) + ); + + assert(error == NULL); + + assert( + symlink( + target_path, + link_path + ) == 0 + ); + + assert( + !file_hash_compute_sha256( + link_path, + NULL, + &sha256, + &size_bytes, + &error + ) + ); + + assert(sha256 == NULL); + assert(size_bytes == 0); + + test_file_hash_assert_error( + error, + FILE_HASH_ERROR_NOT_REGULAR + ); + + g_clear_error( + &error + ); + + assert( + g_remove( + link_path + ) == 0 + ); + + assert( + g_remove( + target_path + ) == 0 + ); + + g_free( + link_path + ); + + g_free( + target_path + ); + + test_file_hash_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie une annulation demandée avant l’ouverture du fichier. + */ +static void test_file_hash_cancelled_before_start(void) +{ + TestFileHashFixture fixture = + test_file_hash_fixture_create(); + + GCancellable *cancellable = NULL; + + char *file_path = NULL; + char *sha256 = NULL; + + guint64 size_bytes = 123; + + GError *error = NULL; + + file_path = + test_file_hash_build_path( + &fixture, + "cancelled.bin" + ); + + assert(file_path != NULL); + + assert( + g_file_set_contents( + file_path, + "abc", + 3, + &error + ) + ); + + assert(error == NULL); + + cancellable = + g_cancellable_new(); + + assert(cancellable != NULL); + + g_cancellable_cancel( + cancellable + ); + + assert( + !file_hash_compute_sha256( + file_path, + cancellable, + &sha256, + &size_bytes, + &error + ) + ); + + assert(sha256 == NULL); + assert(size_bytes == 0); + + test_file_hash_assert_error( + error, + FILE_HASH_ERROR_CANCELLED + ); + + g_clear_error( + &error + ); + + g_object_unref( + cancellable + ); + + assert( + g_remove( + file_path + ) == 0 + ); + + g_free( + file_path + ); + + test_file_hash_fixture_clear( + &fixture + ); +} + +/** + * @brief Annule le calcul après la lecture du premier bloc. + */ +static void test_file_hash_cancel_after_first_block( + guint64 total_size_bytes, + gpointer user_data +) +{ + TestFileHashCancellationContext *context = + user_data; + + assert(context != NULL); + assert(context->cancellable != NULL); + assert(total_size_bytes > 0); + + context->block_count++; + + if (context->block_count == 1) + { + g_cancellable_cancel( + context->cancellable + ); + } +} + +/** + * @brief Vérifie une annulation observée entre deux blocs. + */ +static void test_file_hash_cancelled_during_read(void) +{ + TestFileHashFixture fixture = + test_file_hash_fixture_create(); + + TestFileHashCancellationContext context = {0}; + + const gsize data_size = + 256U * 1024U; + + guint8 *data = NULL; + char *file_path = NULL; + char *sha256 = NULL; + + guint64 size_bytes = 123; + gsize data_index = 0; + + gboolean result = FALSE; + GError *error = NULL; + + file_path = + test_file_hash_build_path( + &fixture, + "cancelled_during_read.bin" + ); + + assert(file_path != NULL); + + data = g_malloc( + data_size + ); + + assert(data != NULL); + + for (data_index = 0; + data_index < data_size; + data_index++) + { + data[data_index] = + (guint8) (data_index % 251U); + } + + assert( + g_file_set_contents( + file_path, + (const char *) data, + (gssize) data_size, + &error + ) + ); + + assert(error == NULL); + + context.cancellable = + g_cancellable_new(); + + assert(context.cancellable != NULL); + + file_hash_test_set_block_hook( + test_file_hash_cancel_after_first_block, + &context + ); + + result = + file_hash_compute_sha256( + file_path, + context.cancellable, + &sha256, + &size_bytes, + &error + ); + + /* + * Le crochet doit toujours être retiré avant de poursuivre les tests. + */ + file_hash_test_set_block_hook( + NULL, + NULL + ); + + assert(!result); + assert(sha256 == NULL); + assert(size_bytes == 0); + assert(context.block_count == 1); + + test_file_hash_assert_error( + error, + FILE_HASH_ERROR_CANCELLED + ); + + g_clear_error( + &error + ); + + g_object_unref( + context.cancellable + ); + + g_free( + data + ); + + assert( + g_remove( + file_path + ) == 0 + ); + + g_free( + file_path + ); + + test_file_hash_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie que plusieurs calculs successifs sont indépendants. + */ +static void test_file_hash_successive_calls(void) +{ + TestFileHashFixture fixture = + test_file_hash_fixture_create(); + + char *file_path = NULL; + char *first_sha256 = NULL; + char *second_sha256 = NULL; + + guint64 first_size_bytes = 0; + guint64 second_size_bytes = 0; + + GError *error = NULL; + + file_path = + test_file_hash_build_path( + &fixture, + "successive_calls.bin" + ); + + assert(file_path != NULL); + + assert( + g_file_set_contents( + file_path, + "contenu stable", + 14, + &error + ) + ); + + assert(error == NULL); + + assert( + file_hash_compute_sha256( + file_path, + NULL, + &first_sha256, + &first_size_bytes, + &error + ) + ); + + assert(error == NULL); + assert(first_sha256 != NULL); + + assert( + file_hash_compute_sha256( + file_path, + NULL, + &second_sha256, + &second_size_bytes, + &error + ) + ); + + assert(error == NULL); + assert(second_sha256 != NULL); + + assert(first_size_bytes == 14); + assert(second_size_bytes == 14); + + assert( + strcmp( + first_sha256, + second_sha256 + ) == 0 + ); + + /* + * Chaque appel doit retourner sa propre chaîne. + */ + assert(first_sha256 != second_sha256); + + g_free( + second_sha256 + ); + + g_free( + first_sha256 + ); + + assert( + g_remove( + file_path + ) == 0 + ); + + g_free( + file_path + ); + + test_file_hash_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie que la modification d’un fichier change son empreinte. + */ +static void test_file_hash_modified_file(void) +{ + TestFileHashFixture fixture = + test_file_hash_fixture_create(); + + char *file_path = NULL; + char *first_sha256 = NULL; + char *second_sha256 = NULL; + + guint64 first_size_bytes = 0; + guint64 second_size_bytes = 0; + + GError *error = NULL; + + file_path = + test_file_hash_build_path( + &fixture, + "modified_file.bin" + ); + + assert(file_path != NULL); + + assert( + g_file_set_contents( + file_path, + "abc", + 3, + &error + ) + ); + + assert(error == NULL); + + assert( + file_hash_compute_sha256( + file_path, + NULL, + &first_sha256, + &first_size_bytes, + &error + ) + ); + + assert(error == NULL); + assert(first_sha256 != NULL); + assert(first_size_bytes == 3); + + assert( + g_file_set_contents( + file_path, + "abcdef", + 6, + &error + ) + ); + + assert(error == NULL); + + assert( + file_hash_compute_sha256( + file_path, + NULL, + &second_sha256, + &second_size_bytes, + &error + ) + ); + + assert(error == NULL); + assert(second_sha256 != NULL); + assert(second_size_bytes == 6); + + assert( + strcmp( + first_sha256, + second_sha256 + ) != 0 + ); + + g_free( + second_sha256 + ); + + g_free( + first_sha256 + ); + + assert( + g_remove( + file_path + ) == 0 + ); + + g_free( + file_path + ); + + test_file_hash_fixture_clear( + &fixture + ); +} + +/** + * @brief Vérifie qu’un tube nommé est refusé sans blocage. + */ +static void test_file_hash_named_pipe(void) +{ + TestFileHashFixture fixture = + test_file_hash_fixture_create(); + + char *pipe_path = NULL; + char *sha256 = NULL; + + guint64 size_bytes = 123; + + GError *error = NULL; + + pipe_path = + test_file_hash_build_path( + &fixture, + "preuve.fifo" + ); + + assert(pipe_path != NULL); + + assert( + mkfifo( + pipe_path, + 0600 + ) == 0 + ); + + assert( + !file_hash_compute_sha256( + pipe_path, + NULL, + &sha256, + &size_bytes, + &error + ) + ); + + assert(sha256 == NULL); + assert(size_bytes == 0); + + test_file_hash_assert_error( + error, + FILE_HASH_ERROR_NOT_REGULAR + ); + + g_clear_error( + &error + ); + + assert( + g_remove( + pipe_path + ) == 0 + ); + + g_free( + pipe_path + ); + + test_file_hash_fixture_clear( + &fixture + ); +} + +int main(void) +{ + test_file_hash_empty_file(); + test_file_hash_known_content(); + test_file_hash_multiple_blocks(); + test_file_hash_binary_content(); + test_file_hash_invalid_arguments(); + test_file_hash_non_null_output(); + test_file_hash_missing_file(); + test_file_hash_directory(); + test_file_hash_symbolic_link(); + test_file_hash_cancelled_before_start(); + test_file_hash_cancelled_during_read(); + test_file_hash_successive_calls(); + test_file_hash_modified_file(); + test_file_hash_named_pipe(); + + printf( + "FileHash : tests des contenus connus valides.\n" + ); + + return 0; +}