feat(core): add path to investigation nodes

This commit is contained in:
grayTerminal-sh 2026-07-14 17:16:21 +02:00
parent 33014a5316
commit 68a624f398
10 changed files with 388 additions and 41 deletions

View file

@ -0,0 +1,241 @@
# Ticket #017
## Titre
Ajouter le chemin complet à `InvestigationNode`.
---
## Objectif
Faire évoluer `InvestigationNode` afin que chaque nœud connaisse le chemin
complet qu'il représente sur le système de fichiers.
Cette information permettra ensuite au `Workspace` et aux futurs visualiseurs
de savoir exactement quel fichier ou dossier est sélectionné.
---
## Responsabilités
Le module `InvestigationNode` doit :
- mémoriser un chemin complet ;
- posséder sa propre copie de ce chemin ;
- exposer le chemin en lecture seule ;
- libérer correctement cette chaîne lors de sa destruction.
Le module `InvestigationTreeBuilder` doit :
- fournir le chemin complet lors de la création de chaque nœud ;
- construire correctement le chemin des enfants ;
- ne jamais reconstruire le chemin dans la GUI.
---
## Architecture
```text
InvestigationTreeBuilder
InvestigationNode
├── name
├── path
├── type
├── parent
└── children
```
Le chemin appartient au Core.
La couche graphique ne fait que le lire.
---
## Fichiers concernés
```text
include/core/investigation_node.h
src/core/investigation_node.c
src/core/investigation_tree_builder.c
tests/test_investigation_node.c
tests/test_investigation_tree_builder.c
```
Aucune modification graphique dans ce ticket.
---
## API publique à faire évoluer
Le constructeur devient :
```c
InvestigationNode *investigation_node_new(
const char *name,
const char *path,
InvestigationNodeType type
);
```
Un nouveau getter est ajouté :
```c
const char *investigation_node_get_path(
const InvestigationNode *node
);
```
---
## Principe de propriété
Le nœud copie le chemin reçu avec une allocation dédiée.
Après :
```c
node = investigation_node_new(
"Enquete.sqlite",
"/home/fy59/Enquetes/Test/00_BaseDeDonnees/Enquete.sqlite",
INVESTIGATION_NODE_FILE
);
```
le code appelant peut modifier ou libérer ses propres chaînes.
Le nœud reste propriétaire de ses copies internes.
---
## Structure interne attendue
```c
struct InvestigationNode
{
char *name;
char *path;
InvestigationNodeType type;
InvestigationNode *parent;
GPtrArray *children;
};
```
La structure reste opaque.
---
## Comportement attendu
Pour le dossier racine :
```text
Nom :
Test
Chemin :
/home/fy59/Enquetes/Test
```
Pour un fichier enfant :
```text
Nom :
Enquete.sqlite
Chemin :
/home/fy59/Enquetes/Test/00_BaseDeDonnees/Enquete.sqlite
```
---
## Règles de validation
Le constructeur doit refuser :
- `name == NULL` ;
- un nom vide ;
- `path == NULL` ;
- un chemin vide.
Le getter doit retourner :
```c
NULL
```
si le nœud vaut `NULL`.
---
## Hors périmètre
Ce ticket ne doit pas :
- afficher le chemin dans le `Workspace` ;
- ouvrir un fichier ;
- normaliser les permissions ;
- résoudre les liens symboliques ;
- calculer un chemin relatif ;
- modifier le système de fichiers ;
- ajouter des métadonnées.
---
## Contraintes techniques
- C17 ;
- GLib autorisée ;
- aucune dépendance GTK ;
- structure opaque ;
- aucun état global ;
- documentation Doxygen ;
- compilation sans warning ;
- aucune fuite mémoire.
---
## Critères d'acceptation
- [ ] Le projet compile sans warning.
- [ ] `InvestigationNode` possède un chemin.
- [ ] Le chemin est copié.
- [ ] Le getter retourne le bon chemin.
- [ ] Les chemins racine et enfants sont corrects.
- [ ] Les noms invalides sont refusés.
- [ ] Les chemins invalides sont refusés.
- [ ] Le builder construit correctement tous les chemins.
- [ ] Tous les tests existants sont adaptés.
- [ ] `make test` reste entièrement valide.
- [ ] Aucun code GTK n'est modifié.
---
## Tests
### InvestigationNode
- créer un nœud avec un chemin valide ;
- lire le chemin ;
- tester `path == NULL` ;
- tester un chemin vide ;
- vérifier que la chaîne est copiée ;
- tester `investigation_node_get_path(NULL)`.
### InvestigationTreeBuilder
- vérifier le chemin de la racine ;
- vérifier le chemin de `DirectoryA` ;
- vérifier le chemin de `FileA.txt` ;
- vérifier le chemin de `RootFile.md`.
---
## Commit attendu
```text
feat(core): add path to investigation nodes
```

View file

@ -6,21 +6,16 @@
#ifndef LABFY_INVESTIGATION_INVESTIGATION_NODE_H #ifndef LABFY_INVESTIGATION_INVESTIGATION_NODE_H
#define LABFY_INVESTIGATION_INVESTIGATION_NODE_H #define LABFY_INVESTIGATION_INVESTIGATION_NODE_H
#include <stdbool.h> #include <stdbool.h>
#include <stddef.h> #include <stddef.h>
/** /**
* @brief Représentation opaque d'un nœud d'enquête. * @brief Représentation opaque d'un nœud.
*
* La structure réelle est définie dans investigation_node.c.
* Les autres modules manipulent uniquement un pointeur vers
* InvestigationNode.
*/ */
typedef struct InvestigationNode InvestigationNode; typedef struct InvestigationNode InvestigationNode;
/** /**
* @brief Type d'un nœud de l'arborescence. * @brief Type d'un nœud.
*/ */
typedef enum typedef enum
{ {
@ -29,24 +24,26 @@ typedef enum
} InvestigationNodeType; } InvestigationNodeType;
/** /**
* @brief Crée un nouveau nœud d'enquête. * @brief Crée un nouveau nœud.
* *
* Le nom fourni est copié. Le code appelant peut donc modifier ou libérer * Le nom et le chemin sont copiés.
* sa chaîne après l'appel sans affecter le nœud. * Le code appelant peut donc modifier ou libérer ses chaînes après l'appel.
* *
* @param name Nom du fichier ou du dossier. * @param name Nom du fichier ou du dossier.
* @param path Chemin complet du fichier ou du dossier.
* @param type Type du nœud. * @param type Type du nœud.
* *
* @return Un nouveau nœud, ou NULL si le nom est invalide ou si la création * @return Un nouveau nœud, ou NULL si le nom ou le chemin est invalide,
* échoue. * ou si une allocation échoue.
*/ */
InvestigationNode *investigation_node_new( InvestigationNode *investigation_node_new(
const char *name, const char *name,
const char *path,
InvestigationNodeType type InvestigationNodeType type
); );
/** /**
* @brief Libère les ressources associées à un nœud. * @brief Libère un nœud et tous ses enfants.
* *
* Cette fonction accepte NULL. * Cette fonction accepte NULL.
* *
@ -57,21 +54,35 @@ void investigation_node_free(
); );
/** /**
* @brief Retourne le nom du nœud. * @brief Retourne le nom d'un nœud.
* *
* La chaîne retournée appartient au nœud et ne doit pas être modifiée * La chaîne retournée appartient au nœud.
* ni libérée par le code appelant. * Elle ne doit être ni modifiée ni libérée.
* *
* @param node Nœud à consulter. * @param node Nœud à consulter.
* *
* @return Le nom en lecture seule, ou NULL si node vaut NULL. * @return Le nom du nœud, ou NULL si node vaut NULL.
*/ */
const char *investigation_node_get_name( const char *investigation_node_get_name(
const InvestigationNode *node const InvestigationNode *node
); );
/** /**
* @brief Retourne le type du nœud. * @brief Retourne le chemin complet d'un nœud.
*
* La chaîne retournée appartient au nœud.
* Elle ne doit être ni modifiée ni libérée.
*
* @param node Nœud à consulter.
*
* @return Le chemin complet, ou NULL si node vaut NULL.
*/
const char *investigation_node_get_path(
const InvestigationNode *node
);
/**
* @brief Retourne le type d'un nœud.
* *
* @param node Nœud à consulter. * @param node Nœud à consulter.
* *
@ -86,7 +97,10 @@ InvestigationNodeType investigation_node_get_type(
* *
* En cas de succès, le parent devient propriétaire de l'enfant. * En cas de succès, le parent devient propriétaire de l'enfant.
* *
* @return true si l'ajout a réussi. * @param parent Nœud parent.
* @param child Nœud enfant.
*
* @return true si l'ajout a réussi, sinon false.
*/ */
bool investigation_node_add_child( bool investigation_node_add_child(
InvestigationNode *parent, InvestigationNode *parent,
@ -94,9 +108,12 @@ bool investigation_node_add_child(
); );
/** /**
* @brief Retourne un enfant. * @brief Retourne un enfant par son index.
* *
* @return L'enfant ou NULL. * @param node Nœud parent.
* @param index Index de l'enfant.
*
* @return L'enfant demandé, ou NULL si l'index est invalide.
*/ */
const InvestigationNode *investigation_node_get_child( const InvestigationNode *investigation_node_get_child(
const InvestigationNode *node, const InvestigationNode *node,
@ -105,6 +122,10 @@ const InvestigationNode *investigation_node_get_child(
/** /**
* @brief Retourne le nombre d'enfants. * @brief Retourne le nombre d'enfants.
*
* @param node Nœud à consulter.
*
* @return Le nombre d'enfants, ou 0 si node vaut NULL.
*/ */
size_t investigation_node_get_children_count( size_t investigation_node_get_children_count(
const InvestigationNode *node const InvestigationNode *node
@ -112,6 +133,11 @@ size_t investigation_node_get_children_count(
/** /**
* @brief Retourne le parent d'un nœud. * @brief Retourne le parent d'un nœud.
*
* @param node Nœud à consulter.
*
* @return Le parent, ou NULL si le nœud n'a pas de parent
* ou si node vaut NULL.
*/ */
const InvestigationNode *investigation_node_get_parent( const InvestigationNode *investigation_node_get_parent(
const InvestigationNode *node const InvestigationNode *node

Binary file not shown.

View file

@ -17,6 +17,7 @@
struct InvestigationNode struct InvestigationNode
{ {
char *name; char *name;
char *path;
InvestigationNodeType type; InvestigationNodeType type;
InvestigationNode *parent; InvestigationNode *parent;
GPtrArray *children; GPtrArray *children;
@ -24,6 +25,7 @@ struct InvestigationNode
InvestigationNode *investigation_node_new( InvestigationNode *investigation_node_new(
const char *name, const char *name,
const char *path,
InvestigationNodeType type InvestigationNodeType type
) )
{ {
@ -34,6 +36,11 @@ InvestigationNode *investigation_node_new(
return NULL; return NULL;
} }
if (path == NULL || path[0] == '\0')
{
return NULL;
}
node = g_new0(InvestigationNode, 1); node = g_new0(InvestigationNode, 1);
if (node == NULL) if (node == NULL)
@ -49,6 +56,14 @@ InvestigationNode *investigation_node_new(
return NULL; return NULL;
} }
node->path = g_strdup(path);
if (node->path == NULL)
{
investigation_node_free(node);
return NULL;
}
node->children = g_ptr_array_new_with_free_func( node->children = g_ptr_array_new_with_free_func(
(GDestroyNotify) investigation_node_free (GDestroyNotify) investigation_node_free
); );
@ -82,10 +97,23 @@ void investigation_node_free(
g_ptr_array_free(node->children, TRUE); g_ptr_array_free(node->children, TRUE);
} }
g_free(node->path);
g_free(node->name); g_free(node->name);
g_free(node); g_free(node);
} }
const char *investigation_node_get_path(
const InvestigationNode *node
)
{
if (node == NULL)
{
return NULL;
}
return node->path;
}
const char *investigation_node_get_name( const char *investigation_node_get_name(
const InvestigationNode *node const InvestigationNode *node
) )

View file

@ -64,6 +64,7 @@ static bool investigation_tree_builder_build_children(
GFileType child_file_type; GFileType child_file_type;
GFile *child_file = NULL; GFile *child_file = NULL;
InvestigationNode *child_node = NULL; InvestigationNode *child_node = NULL;
char *child_path = NULL;
InvestigationNodeType child_node_type; InvestigationNodeType child_node_type;
file_info = g_file_enumerator_next_file( file_info = g_file_enumerator_next_file(
@ -118,36 +119,6 @@ static bool investigation_tree_builder_build_children(
child_node_type = INVESTIGATION_NODE_FILE; 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( child_file = g_file_get_child(
directory, directory,
child_name child_name
@ -161,16 +132,55 @@ static bool investigation_tree_builder_build_children(
break; break;
} }
child_path = g_file_get_path(child_file);
if (child_path == NULL)
{
success = false;
g_object_unref(child_file);
g_object_unref(file_info);
file_info = NULL;
break;
}
child_node = investigation_node_new(
child_name,
child_path,
child_node_type
);
g_free(child_path);
child_path = NULL;
/*
* 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(child_file);
g_object_unref(file_info);
file_info = NULL;
break;
}
/*
* Seuls les dossiers sont parcourus récursivement.
*/
if (child_node_type == INVESTIGATION_NODE_DIRECTORY)
{
success = investigation_tree_builder_build_children( success = investigation_tree_builder_build_children(
child_file, child_file,
child_node, child_node,
error error
); );
g_object_unref(child_file);
} }
g_object_unref(child_file);
g_object_unref(file_info); g_object_unref(file_info);
child_file = NULL;
file_info = NULL; file_info = NULL;
} }
@ -234,6 +244,7 @@ InvestigationTreeModel *investigation_tree_builder_build(
root_node = investigation_node_new( root_node = investigation_node_new(
root_name, root_name,
root_path,
INVESTIGATION_NODE_DIRECTORY INVESTIGATION_NODE_DIRECTORY
); );

Binary file not shown.

View file

@ -15,6 +15,7 @@ static void test_directory_node(void)
node = investigation_node_new( node = investigation_node_new(
"01_Preuves_Originales", "01_Preuves_Originales",
"/test/01_Preuves_Originales",
INVESTIGATION_NODE_DIRECTORY INVESTIGATION_NODE_DIRECTORY
); );
@ -39,6 +40,7 @@ static void test_file_node(void)
node = investigation_node_new( node = investigation_node_new(
"Chronologie.md", "Chronologie.md",
"/test/Chronologie.md",
INVESTIGATION_NODE_FILE INVESTIGATION_NODE_FILE
); );
@ -64,16 +66,19 @@ static void test_add_children(void)
root = investigation_node_new( root = investigation_node_new(
"Template", "Template",
"/test/Template",
INVESTIGATION_NODE_DIRECTORY INVESTIGATION_NODE_DIRECTORY
); );
database_directory = investigation_node_new( database_directory = investigation_node_new(
"00_BaseDeDonnees", "00_BaseDeDonnees",
"/test/Template/00_BaseDeDonnees",
INVESTIGATION_NODE_DIRECTORY INVESTIGATION_NODE_DIRECTORY
); );
database_file = investigation_node_new( database_file = investigation_node_new(
"Enquete.sqlite", "Enquete.sqlite",
"/test/Template/00_BaseDeDonnees/Enquete.sqlite",
INVESTIGATION_NODE_FILE INVESTIGATION_NODE_FILE
); );
@ -131,21 +136,25 @@ static void test_invalid_additions(void)
directory = investigation_node_new( directory = investigation_node_new(
"Directory", "Directory",
"/test/Directory",
INVESTIGATION_NODE_DIRECTORY INVESTIGATION_NODE_DIRECTORY
); );
file = investigation_node_new( file = investigation_node_new(
"File.txt", "File.txt",
"/test/File.txt",
INVESTIGATION_NODE_FILE INVESTIGATION_NODE_FILE
); );
child = investigation_node_new( child = investigation_node_new(
"Child", "Child",
"/test/Directory/Child",
INVESTIGATION_NODE_DIRECTORY INVESTIGATION_NODE_DIRECTORY
); );
second_parent = investigation_node_new( second_parent = investigation_node_new(
"SecondParent", "SecondParent",
"/test/SecondParent",
INVESTIGATION_NODE_DIRECTORY INVESTIGATION_NODE_DIRECTORY
); );
@ -177,6 +186,7 @@ static void test_invalid_index(void)
node = investigation_node_new( node = investigation_node_new(
"Template", "Template",
"/test/Template",
INVESTIGATION_NODE_DIRECTORY INVESTIGATION_NODE_DIRECTORY
); );
@ -194,12 +204,33 @@ static void test_invalid_names(void)
assert( assert(
investigation_node_new( investigation_node_new(
NULL, NULL,
"/test/file",
INVESTIGATION_NODE_FILE INVESTIGATION_NODE_FILE
) == NULL ) == NULL
); );
assert( assert(
investigation_node_new( investigation_node_new(
"",
"/test/file",
INVESTIGATION_NODE_FILE
) == NULL
);
}
static void test_invalid_paths(void)
{
assert(
investigation_node_new(
"File.txt",
NULL,
INVESTIGATION_NODE_FILE
) == NULL
);
assert(
investigation_node_new(
"File.txt",
"", "",
INVESTIGATION_NODE_FILE INVESTIGATION_NODE_FILE
) == NULL ) == NULL
@ -211,6 +242,7 @@ static void test_null_behaviour(void)
assert(investigation_node_get_name(NULL) == NULL); assert(investigation_node_get_name(NULL) == NULL);
assert(investigation_node_get_children_count(NULL) == 0); assert(investigation_node_get_children_count(NULL) == 0);
assert(investigation_node_get_parent(NULL) == NULL); assert(investigation_node_get_parent(NULL) == NULL);
assert(investigation_node_get_path(NULL) == NULL);
investigation_node_free(NULL); investigation_node_free(NULL);
} }
@ -224,6 +256,7 @@ int main(void)
test_invalid_index(); test_invalid_index();
test_invalid_names(); test_invalid_names();
test_null_behaviour(); test_null_behaviour();
test_invalid_paths();
printf( printf(
"InvestigationNode : tous les tests de hiérarchie sont valides.\n" "InvestigationNode : tous les tests de hiérarchie sont valides.\n"

Binary file not shown.

View file

@ -21,6 +21,7 @@ static void test_tree_model_creation(void)
root_node = investigation_node_new( root_node = investigation_node_new(
"Template", "Template",
"/test/Template",
INVESTIGATION_NODE_DIRECTORY INVESTIGATION_NODE_DIRECTORY
); );
@ -41,6 +42,13 @@ static void test_tree_model_creation(void)
) == 0 ) == 0
); );
assert(
strcmp(
investigation_node_get_path(returned_node),
"/test/Template"
) == 0
);
assert( assert(
investigation_node_get_type(returned_node) == investigation_node_get_type(returned_node) ==
INVESTIGATION_NODE_DIRECTORY INVESTIGATION_NODE_DIRECTORY