diff --git a/Makefile b/Makefile index f5307e6..259c841 100644 --- a/Makefile +++ b/Makefile @@ -19,9 +19,9 @@ TEST_CFLAGS = -std=c17 \ -Wextra \ -Werror \ -Iinclude \ - $(shell $(PKG_CONFIG) --cflags glib-2.0) + $(shell $(PKG_CONFIG) --cflags glib-2.0 gio-2.0) -TEST_LDFLAGS = $(shell $(PKG_CONFIG) --libs glib-2.0) +TEST_LDFLAGS = $(shell $(PKG_CONFIG) --libs glib-2.0 gio-2.0) SRC := $(shell find src -name "*.c") @@ -31,6 +31,7 @@ TARGET = labfy-investigation TEST_NODE = tests/test_investigation_node TEST_TREE_MODEL = tests/test_investigation_tree_model +TEST_TREE_BUILDER = tests/test_investigation_tree_builder all: $(TARGET) @@ -48,10 +49,19 @@ $(TEST_TREE_MODEL): \ src/core/investigation_tree_model.c $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) -test: $(TEST_NODE) $(TEST_TREE_MODEL) +$(TEST_TREE_BUILDER): \ + tests/test_investigation_tree_builder.c \ + src/core/investigation_node.c \ + src/core/investigation_tree_model.c \ + src/core/investigation_tree_builder.c + $(CC) $(TEST_CFLAGS) $^ -o $@ $(TEST_LDFLAGS) \ + $(shell $(PKG_CONFIG) --libs gio-2.0) + +test: $(TEST_NODE) $(TEST_TREE_MODEL) $(TEST_TREE_BUILDER) @echo "Exécution des tests..." @./$(TEST_NODE) @./$(TEST_TREE_MODEL) + @./$(TEST_TREE_BUILDER) @echo "Tous les tests sont valides." %.o: %.c @@ -63,6 +73,7 @@ run: $(TARGET) clean: rm -f $(OBJ) $(TARGET) \ $(TEST_NODE) \ - $(TEST_TREE_MODEL) + $(TEST_TREE_MODEL) \ + $(TEST_TREE_BUILDER) .PHONY: clean run test diff --git a/docs/tickets/closed/TICKET-010.md b/docs/tickets/closed/TICKET-010.md new file mode 100644 index 0000000..b4aae49 --- /dev/null +++ b/docs/tickets/closed/TICKET-010.md @@ -0,0 +1,243 @@ +# Ticket #010 + +## Titre + +Construire l'arborescence d'une enquête depuis le système de fichiers. + +--- + +## Objectif + +Créer le module `InvestigationTreeBuilder`. + +Ce module doit parcourir récursivement le dossier racine d'une enquête et +construire un `InvestigationTreeModel` représentant son contenu. + +--- + +## Responsabilités + +Le module `InvestigationTreeBuilder` doit : + +- recevoir le chemin racine d'une enquête ; +- vérifier que ce chemin désigne un dossier existant ; +- créer le nœud racine ; +- parcourir récursivement les dossiers ; +- créer un `InvestigationNode` pour chaque dossier ; +- créer un `InvestigationNode` pour chaque fichier ; +- assembler les relations parent/enfants ; +- retourner un `InvestigationTreeModel` complet ; +- nettoyer toutes les ressources en cas d'erreur. + +--- + +## Hors périmètre + +Ce ticket ne doit pas : + +- afficher l'arborescence avec GTK ; +- modifier le système de fichiers ; +- créer ou supprimer des fichiers ; +- trier les éléments ; +- filtrer les fichiers cachés ; +- surveiller les changements du disque ; +- suivre les liens symboliques ; +- communiquer avec SQLite. + +--- + +## Architecture + +```text +Investigation + │ + ▼ +InvestigationTreeBuilder + │ + ▼ +InvestigationTreeModel + │ + ▼ +InvestigationNode +``` + +Le module appartient à la couche `core`. + +Il peut utiliser GLib et GIO, mais ne doit jamais dépendre de GTK. + +--- + +## Fichiers concernés + +```text +include/core/investigation_tree_builder.h +src/core/investigation_tree_builder.c +``` + +Les tests seront placés dans : + +```text +tests/test_investigation_tree_builder.c +``` + +--- + +## Interface publique attendue + +```c +InvestigationTreeModel *investigation_tree_builder_build( + const char *root_path +); +``` + +--- + +## Principe de propriété + +En cas de succès, la fonction retourne un nouveau +`InvestigationTreeModel`. + +Le code appelant devient propriétaire du modèle retourné et doit le libérer +avec : + +```c +investigation_tree_model_free(tree_model); +``` + +En cas d'échec, la fonction retourne `NULL` et doit avoir libéré toutes les +ressources créées pendant la construction. + +--- + +## Comportement attendu + +À partir de : + +```text +Enquete_Test/ +├── 00_BaseDeDonnees/ +│ └── Enquete.sqlite +├── 01_Preuves_Originales/ +│ ├── Captures_Ecran/ +│ └── Emails/ +└── README.md +``` + +le module doit construire en mémoire : + +```text +Enquete_Test +├── 00_BaseDeDonnees +│ └── Enquete.sqlite +├── 01_Preuves_Originales +│ ├── Captures_Ecran +│ └── Emails +└── README.md +``` + +--- + +## Gestion des liens symboliques + +Les liens symboliques ne sont pas suivis. + +Cette règle évite : + +- les boucles récursives ; +- la sortie involontaire du dossier d'enquête ; +- l'analyse de fichiers extérieurs à l'enquête. + +Leur prise en charge éventuelle fera l'objet d'un ticket distinct. + +--- + +## Dépendances + +- C17 ; +- GLib ; +- GIO. + +Aucune dépendance GTK ou SQLite. + +--- + +## Contraintes techniques + +- aucune variable globale ; +- aucune modification du système de fichiers ; +- parcours récursif ; +- utilisation de `GFile` et `GFileEnumerator` ; +- libération correcte des `GObject` avec `g_object_unref()` ; +- respect de la règle « le propriétaire détruit » ; +- documentation Doxygen ; +- compilation sans warning ; +- aucune fuite mémoire. + +--- + +## Cas à gérer + +- chemin valide ; +- chemin relatif ; +- chemin `NULL` ; +- chemin vide ; +- chemin inexistant ; +- chemin désignant un fichier ; +- dossier vide ; +- plusieurs niveaux de sous-dossiers ; +- fichiers et dossiers mélangés ; +- erreur rencontrée pendant le parcours. + +--- + +## Critères d'acceptation + +- [ ] Le projet compile sans warning. +- [ ] Un dossier valide produit un modèle. +- [ ] Le nom du dossier racine est correct. +- [ ] Les dossiers produisent des nœuds de type `DIRECTORY`. +- [ ] Les fichiers produisent des nœuds de type `FILE`. +- [ ] Les relations parent/enfants sont correctes. +- [ ] Plusieurs niveaux de profondeur sont pris en charge. +- [ ] Un dossier vide est pris en charge. +- [ ] Un chemin invalide retourne `NULL`. +- [ ] Les liens symboliques ne sont pas suivis. +- [ ] Toutes les ressources sont libérées en cas d'erreur. +- [ ] Aucun code GTK. +- [ ] Aucun code SQLite. +- [ ] `make test` exécute le nouveau test. + +--- + +## Tests + +Le test doit créer une arborescence temporaire isolée : + +```text +TestCase/ +├── DirectoryA/ +│ └── FileA.txt +├── DirectoryB/ +└── RootFile.md +``` + +Il doit vérifier : + +- le nom du nœud racine ; +- le nombre d'enfants de la racine ; +- la présence des deux dossiers ; +- la présence du fichier racine ; +- la présence de `FileA.txt` dans `DirectoryA` ; +- le type de chaque nœud ; +- le parent de chaque enfant ; +- la gestion d'un dossier vide ; +- les chemins invalides ; +- la destruction complète du modèle. + +--- + +## Commit attendu + +```text +feat(core): build investigation tree from filesystem +``` diff --git a/include/core/investigation_tree_builder.h b/include/core/investigation_tree_builder.h new file mode 100644 index 0000000..dd47ecc --- /dev/null +++ b/include/core/investigation_tree_builder.h @@ -0,0 +1,36 @@ +/****************************************************************************** + * @file investigation_tree_builder.h + * @brief Interface publique du constructeur d'arborescence d'une enquête. + ******************************************************************************/ + +#ifndef LABFY_INVESTIGATION_INVESTIGATION_TREE_BUILDER_H +#define LABFY_INVESTIGATION_INVESTIGATION_TREE_BUILDER_H + +#include "core/investigation_tree_model.h" + +/** + * @brief Construit le modèle d'arborescence d'une enquête depuis le disque. + * + * Le chemin fourni doit désigner un dossier existant. + * + * La fonction parcourt récursivement son contenu et crée : + * + * - un nœud racine pour le dossier sélectionné ; + * - un nœud de type DIRECTORY pour chaque sous-dossier ; + * - un nœud de type FILE pour chaque fichier. + * + * Les liens symboliques ne sont pas suivis. + * + * Le code appelant devient propriétaire du modèle retourné et doit le + * libérer avec investigation_tree_model_free(). + * + * @param root_path Chemin du dossier racine à parcourir. + * + * @return Un nouveau modèle d'arborescence, ou NULL si le chemin est invalide + * ou si une erreur empêche la construction du modèle. + */ +InvestigationTreeModel *investigation_tree_builder_build( + const char *root_path +); + +#endif diff --git a/labfy-investigation b/labfy-investigation index 521ce43..6a7dc5b 100755 Binary files a/labfy-investigation and b/labfy-investigation differ diff --git a/src/core/investigation_tree_builder.c b/src/core/investigation_tree_builder.c new file mode 100644 index 0000000..3c76f54 --- /dev/null +++ b/src/core/investigation_tree_builder.c @@ -0,0 +1,285 @@ +/****************************************************************************** + * @file investigation_tree_builder.c + * @brief Construction d'une arborescence d'enquête depuis le système de fichiers. + ******************************************************************************/ + +#include "core/investigation_tree_builder.h" + +#include "core/investigation_node.h" +#include "core/investigation_tree_model.h" + +#include + +#include +#include + +/** + * @brief Attributs demandés à GIO pendant le parcours d'un dossier. + */ +#define INVESTIGATION_TREE_FILE_ATTRIBUTES \ + G_FILE_ATTRIBUTE_STANDARD_NAME "," \ + G_FILE_ATTRIBUTE_STANDARD_TYPE "," \ + G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK + +/** + * @brief Construit récursivement les enfants d'un nœud dossier. + * + * @param directory Dossier GIO actuellement parcouru. + * @param parent_node Nœud représentant ce dossier dans notre modèle. + * @param error Adresse recevant une éventuelle erreur GIO. + * + * @return true si le parcours s'est terminé correctement, sinon false. + */ +static bool investigation_tree_builder_build_children( + GFile *directory, + InvestigationNode *parent_node, + GError **error +) +{ + GFileEnumerator *enumerator = NULL; + GFileInfo *file_info = NULL; + bool success = true; + + if (directory == NULL || parent_node == NULL) + { + return false; + } + + enumerator = g_file_enumerate_children( + directory, + INVESTIGATION_TREE_FILE_ATTRIBUTES, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + NULL, + error + ); + + if (enumerator == NULL) + { + return false; + } + + while (success) + { + const char *child_name = NULL; + GFileType child_file_type; + GFile *child_file = NULL; + InvestigationNode *child_node = NULL; + InvestigationNodeType child_node_type; + + file_info = g_file_enumerator_next_file( + enumerator, + NULL, + error + ); + + if (file_info == NULL) + { + /* + * NULL signifie soit que le dossier est entièrement parcouru, + * soit qu'une erreur a eu lieu. + */ + if (error != NULL && *error != NULL) + { + success = false; + } + + break; + } + + /* + * Les liens symboliques sont volontairement ignorés afin d'éviter + * les boucles et les sorties involontaires du dossier d'enquête. + */ + if (g_file_info_get_is_symlink(file_info)) + { + g_object_unref(file_info); + file_info = NULL; + continue; + } + + child_name = g_file_info_get_name(file_info); + + if (child_name == NULL || child_name[0] == '\0') + { + success = false; + g_object_unref(file_info); + file_info = NULL; + break; + } + + child_file_type = g_file_info_get_file_type(file_info); + + if (child_file_type == G_FILE_TYPE_DIRECTORY) + { + child_node_type = INVESTIGATION_NODE_DIRECTORY; + } + else + { + child_node_type = INVESTIGATION_NODE_FILE; + } + + child_node = investigation_node_new( + child_name, + child_node_type + ); + + if (child_node == NULL) + { + success = false; + g_object_unref(file_info); + file_info = NULL; + break; + } + + /* + * En cas de succès, parent_node devient propriétaire de child_node. + */ + if (!investigation_node_add_child(parent_node, child_node)) + { + investigation_node_free(child_node); + success = false; + g_object_unref(file_info); + file_info = NULL; + break; + } + + /* + * Seuls les dossiers sont parcourus récursivement. + */ + if (child_node_type == INVESTIGATION_NODE_DIRECTORY) + { + child_file = g_file_get_child( + directory, + child_name + ); + + if (child_file == NULL) + { + success = false; + g_object_unref(file_info); + file_info = NULL; + break; + } + + success = investigation_tree_builder_build_children( + child_file, + child_node, + error + ); + + g_object_unref(child_file); + } + + g_object_unref(file_info); + file_info = NULL; + } + + /* + * Si la boucle a été interrompue avant la libération de file_info, + * cette vérification garantit un nettoyage correct. + */ + if (file_info != NULL) + { + g_object_unref(file_info); + } + + g_object_unref(enumerator); + + return success; +} + +InvestigationTreeModel *investigation_tree_builder_build( + const char *root_path +) +{ + GFile *root_file = NULL; + GFileType root_file_type; + char *root_name = NULL; + InvestigationNode *root_node = NULL; + InvestigationTreeModel *tree_model = NULL; + GError *error = NULL; + + if (root_path == NULL || root_path[0] == '\0') + { + return NULL; + } + + root_file = g_file_new_for_path(root_path); + + if (root_file == NULL) + { + return NULL; + } + + root_file_type = g_file_query_file_type( + root_file, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, + NULL + ); + + if (root_file_type != G_FILE_TYPE_DIRECTORY) + { + g_object_unref(root_file); + return NULL; + } + + root_name = g_file_get_basename(root_file); + + if (root_name == NULL || root_name[0] == '\0') + { + g_free(root_name); + g_object_unref(root_file); + return NULL; + } + + root_node = investigation_node_new( + root_name, + INVESTIGATION_NODE_DIRECTORY + ); + + g_free(root_name); + + if (root_node == NULL) + { + g_object_unref(root_file); + return NULL; + } + + if (!investigation_tree_builder_build_children( + root_file, + root_node, + &error + )) + { + if (error != NULL) + { + g_warning( + "Impossible de construire l'arborescence : %s", + error->message + ); + + g_clear_error(&error); + } + + investigation_node_free(root_node); + g_object_unref(root_file); + + return NULL; + } + + g_object_unref(root_file); + + tree_model = investigation_tree_model_new(root_node); + + if (tree_model == NULL) + { + /* + * Le transfert de propriété n'a pas eu lieu puisque la création + * du modèle a échoué. + */ + investigation_node_free(root_node); + return NULL; + } + + return tree_model; +} diff --git a/tests/test_investigation_node b/tests/test_investigation_node index fc36224..9cb1d29 100755 Binary files a/tests/test_investigation_node and b/tests/test_investigation_node differ diff --git a/tests/test_investigation_tree_builder b/tests/test_investigation_tree_builder new file mode 100755 index 0000000..fd9d901 Binary files /dev/null and b/tests/test_investigation_tree_builder differ diff --git a/tests/test_investigation_tree_builder.c b/tests/test_investigation_tree_builder.c new file mode 100644 index 0000000..801dee7 --- /dev/null +++ b/tests/test_investigation_tree_builder.c @@ -0,0 +1,338 @@ +/****************************************************************************** + * @file test_investigation_tree_builder.c + * @brief Tests d'intégration du module InvestigationTreeBuilder. + ******************************************************************************/ + +#include "core/investigation_node.h" +#include "core/investigation_tree_builder.h" +#include "core/investigation_tree_model.h" + +#include +#include +#include + +#include +#include + +/** + * @brief Recherche un enfant par son nom. + * + * @param parent Nœud dans lequel effectuer la recherche. + * @param name Nom recherché. + * + * @return L'enfant correspondant, ou NULL s'il n'existe pas. + */ +static const InvestigationNode *test_find_child( + const InvestigationNode *parent, + const char *name +) +{ + size_t child_count = 0; + + if (parent == NULL || name == NULL) + { + return NULL; + } + + child_count = investigation_node_get_children_count(parent); + + for (size_t index = 0; index < child_count; ++index) + { + const InvestigationNode *child = NULL; + const char *child_name = NULL; + + child = investigation_node_get_child(parent, index); + + if (child == NULL) + { + continue; + } + + child_name = investigation_node_get_name(child); + + if (child_name != NULL && strcmp(child_name, name) == 0) + { + return child; + } + } + + return NULL; +} + +/** + * @brief Crée un fichier texte utilisé par le test. + */ +static void test_create_file(const char *file_path) +{ + GError *error = NULL; + + assert(file_path != NULL); + + assert( + g_file_set_contents( + file_path, + "test\n", + -1, + &error + ) + ); + + assert(error == NULL); +} + +/** + * @brief Vérifie la construction récursive d'une arborescence. + */ +static void test_build_tree_from_filesystem(void) +{ + char *root_path = NULL; + char *root_name = NULL; + + char *directory_a_path = NULL; + char *directory_b_path = NULL; + char *file_a_path = NULL; + char *root_file_path = NULL; + + GError *error = NULL; + + InvestigationTreeModel *tree_model = NULL; + const InvestigationNode *root_node = NULL; + const InvestigationNode *directory_a_node = NULL; + const InvestigationNode *directory_b_node = NULL; + const InvestigationNode *file_a_node = NULL; + const InvestigationNode *root_file_node = NULL; + + /* + * GLib crée un dossier temporaire unique, par exemple : + * + * /tmp/labfy-investigation-test-ABC123 + */ + root_path = g_dir_make_tmp( + "labfy-investigation-test-XXXXXX", + &error + ); + + assert(root_path != NULL); + assert(error == NULL); + + directory_a_path = g_build_filename( + root_path, + "DirectoryA", + NULL + ); + + directory_b_path = g_build_filename( + root_path, + "DirectoryB", + NULL + ); + + file_a_path = g_build_filename( + directory_a_path, + "FileA.txt", + NULL + ); + + root_file_path = g_build_filename( + root_path, + "RootFile.md", + NULL + ); + + assert(g_mkdir(directory_a_path, 0700) == 0); + assert(g_mkdir(directory_b_path, 0700) == 0); + + test_create_file(file_a_path); + test_create_file(root_file_path); + + /* + * Arborescence créée : + * + * racine temporaire/ + * ├── DirectoryA/ + * │ └── FileA.txt + * ├── DirectoryB/ + * └── RootFile.md + */ + tree_model = investigation_tree_builder_build(root_path); + + assert(tree_model != NULL); + + root_node = investigation_tree_model_get_root(tree_model); + + assert(root_node != NULL); + assert( + investigation_node_get_type(root_node) == + INVESTIGATION_NODE_DIRECTORY + ); + + root_name = g_path_get_basename(root_path); + + assert(root_name != NULL); + assert( + strcmp( + investigation_node_get_name(root_node), + root_name + ) == 0 + ); + + assert(investigation_node_get_children_count(root_node) == 3); + + /* + * L'ordre retourné par le système de fichiers n'est pas garanti. + * On recherche donc chaque enfant par son nom. + */ + directory_a_node = test_find_child( + root_node, + "DirectoryA" + ); + + directory_b_node = test_find_child( + root_node, + "DirectoryB" + ); + + root_file_node = test_find_child( + root_node, + "RootFile.md" + ); + + assert(directory_a_node != NULL); + assert(directory_b_node != NULL); + assert(root_file_node != NULL); + + assert( + investigation_node_get_type(directory_a_node) == + INVESTIGATION_NODE_DIRECTORY + ); + + assert( + investigation_node_get_type(directory_b_node) == + INVESTIGATION_NODE_DIRECTORY + ); + + assert( + investigation_node_get_type(root_file_node) == + INVESTIGATION_NODE_FILE + ); + + assert( + investigation_node_get_parent(directory_a_node) == + root_node + ); + + assert( + investigation_node_get_parent(directory_b_node) == + root_node + ); + + assert( + investigation_node_get_parent(root_file_node) == + root_node + ); + + /* + * DirectoryB est vide. + */ + assert( + investigation_node_get_children_count(directory_b_node) == 0 + ); + + /* + * DirectoryA contient FileA.txt. + */ + assert( + investigation_node_get_children_count(directory_a_node) == 1 + ); + + file_a_node = test_find_child( + directory_a_node, + "FileA.txt" + ); + + assert(file_a_node != NULL); + + assert( + investigation_node_get_type(file_a_node) == + INVESTIGATION_NODE_FILE + ); + + assert( + investigation_node_get_parent(file_a_node) == + directory_a_node + ); + + /* + * Le modèle possède la racine et toute son arborescence. + */ + investigation_tree_model_free(tree_model); + + /* + * Nettoyage du système de fichiers dans l'ordre inverse + * de la création. + */ + assert(g_remove(file_a_path) == 0); + assert(g_remove(root_file_path) == 0); + assert(g_rmdir(directory_a_path) == 0); + assert(g_rmdir(directory_b_path) == 0); + assert(g_rmdir(root_path) == 0); + + g_free(root_file_path); + g_free(file_a_path); + g_free(directory_b_path); + g_free(directory_a_path); + g_free(root_name); + g_free(root_path); +} + +/** + * @brief Vérifie les chemins invalides. + */ +static void test_invalid_paths(void) +{ + char *temporary_file_path = NULL; + GError *error = NULL; + + assert(investigation_tree_builder_build(NULL) == NULL); + assert(investigation_tree_builder_build("") == NULL); + + assert( + investigation_tree_builder_build( + "/chemin/qui/nexiste/pas" + ) == NULL + ); + + temporary_file_path = g_build_filename( + g_get_tmp_dir(), + "labfy-investigation-builder-file-test.txt", + NULL + ); + + test_create_file(temporary_file_path); + + /* + * Le chemin existe, mais il désigne un fichier et non un dossier. + */ + assert( + investigation_tree_builder_build( + temporary_file_path + ) == NULL + ); + + assert(g_remove(temporary_file_path) == 0); + + g_free(temporary_file_path); + g_clear_error(&error); +} + +int main(void) +{ + test_build_tree_from_filesystem(); + test_invalid_paths(); + + printf( + "InvestigationTreeBuilder : tous les tests sont valides.\n" + ); + + return 0; +} diff --git a/tests/test_investigation_tree_model b/tests/test_investigation_tree_model index 298935d..a439b76 100755 Binary files a/tests/test_investigation_tree_model and b/tests/test_investigation_tree_model differ