feat(core): build investigation tree from filesystem

This commit is contained in:
grayTerminal-sh 2026-07-13 16:14:41 +02:00
parent 2bd29e6208
commit a0e6743ac8
9 changed files with 917 additions and 4 deletions

View file

@ -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

View file

@ -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
```

View file

@ -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

Binary file not shown.

View file

@ -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 <stdbool.h>
#include <gio/gio.h>
#include <glib.h>
/**
* @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 é 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;
}

Binary file not shown.

Binary file not shown.

View file

@ -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 <assert.h>
#include <stdio.h>
#include <string.h>
#include <glib.h>
#include <glib/gstdio.h>
/**
* @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;
}

Binary file not shown.