BÉDEX Antoine
BAICHOO Esteban
Ce rapport a pour but de vous présenter le projet réalisé dans le cadre du cours de "Méthodologie de la Programmation" à l'ENSEEIHT, Toulouse.
Bien que ce projet soit unique, les professeurs nous ont autorisé à travailler en équipe, à condition que chaque membre de l'équipe participe au développement. Ce projet a donc été réalisé par Antoine BÉDEX et Esteban BAICHOO.
Ce rapport abordera toutes les parties de la réalisation du projet, allant de la présentation des choix réalisés à la présentation (succincte) des algorithmes et types de données utilisés, en passant par la démarche adoptée et les difficultés rencontrées.
Le projet proposé consiste à développer un Système de Gestion de Fichiers (SGF) similaire à ceux retrouvables sur des systèmes d'exploitations tels que UNIX. Un SGF permet l'organisation des répertoires et fichiers utilisateurs de manière hiérarchique. Un SGF gère également la manipulation des fichiers et des répertoires, ainsi que la gestion de l'espace mémoire occupé par ces derniers. Chaque répertoire et fichier est muni de données (tailles, poids, chemin d'accès, parent) appelées "métadonnées".
Dans le cadre du projet, il a été demandé de programmer un SGF ainsi que les commandes qui lui sont associées. Pour cela, deux "mini-interpréteurs de commande" ont été réalisés, l'un offrant un menu semi-graphique tandis que l'autre offre une interface proche des terminaux classiques.
Le langage de support proposé est le langage Ada, utilisé lors des enseignements de ce cours.
Le rapport proposé suivra ce plan :
[TOC]
Pour chaque partie de ce rapport, les modules seront traités un à un.
Afin de permettre une modularité importante, il a été décidé de décomposer un maximum le projet en plusieurs modules Ada. Dans cette partie, les différents modules développés seront présentés de façon succincte. Il ne sera pas fait état de l'utilisation des bibliothèque classiques d'Ada, telles que Ada.Text_IO
, puisque l'utilisation de celles-ci est obligatoire. Voici les paquetages développés :
-
P_Constants
: Paquetage contenant toutes les constantes utilisées par les paquetages du programme. Ce paquetage permet de s'assurer que la modification d'une constante (telle que la taille max d'uneString
, par exemple) se répercutera sur l'ensemble du programme. -
P_Array
: Paquetage contenant la définition d'une structure de données de type tableau appeléeT_Array
, ainsi que les opérations applicables sur un tableau telles que l'ajout et la suppression d'une valeur, la consultation du nombre de valeurs, etc... -
P_Tree
: Paquetage contenant une structure de données de type arbre, tel que stipulé dans le sujet.P_Tree
contient également un ensemble de sous-programmes applicables sur un arbre, tels que l'ajout d'un enfant, la possibilité d'obtenir un parent, de savoir si un arbre est vide, etc... -
P_Metadata
: Paquetage contenant une structure de données de type enregistrementT_Metadata
ainsi que les opérations s'appliquant dessus. Ce paquetage vise à représenter "l'information sur l'information" des dossiers et des fichiers. Les métadonnées contiennent toutes un nom, une taille, des droits et un chemin d'accès. Les opérations du paquetage permettent de créer (create
) puis de modifier (set
) et de consulter (get
) un typeT_Metadata
. -
P_File
: Paquetage permettant de représenter un fichier. Il contient un typeT_File
qui sera détaillé ultérieurement, ainsi que les opérations applicables sur un fichier telles que sa création et sa modification. -
P_Folder
: Ce paquetage contient un paquetageP_Folder_Tree
qui est une instanciation du paquetage génériqueP_Tree
, ainsi que les opérations applicables sur unP_Folder_Tree.T_Tree
, c'est à dire, un type dossier.P_Folder
permet la modification d'un dossier, via l'ajout de fichiers ou de sous-dossiers par exemple. -
P_Substrings
: Paquetage contenant des opérations applicables sur desString
telles que le découpage selon unCharacter
spécifique. Ce paquetage a été développé dans le but de "parser" plus facilement les lignes de commande saisies par l'utilisateur. Il contient un typeT_Substrings
qui sera détaillé ultérieurement. -
P_Commands
: Paquetage le plus volumineux du projet, il contient l'ensemble des commandes définies dans le sujet telles quels
,cp
,mv
, etc... ainsi que des sous-programmes résultants de la factorisation de ces commandes.
Deux programmes principaux ont également été développés :
menu
: Il s'agit d'un menu semi-graphique permettant à l'utilisateur d'effectuer une commande parmi une liste de choix proposés.terminal
: Il s'agit d'un interpréteur de commande classique tel qu'on peut le retrouve sur les systèmes d'exploitation de type Unix.
Le dossier tests
contient un ensemble de programmes de tests (appelés test_*.adb
) permettant de tester les paquetages précédents.
Voici un diagramme permettant de représenter l'architecture de l'application. Afin de soulager la lecture, il a été décidé de faire fi du paquetage P_Constants
(qui est utilisé par presque tous les autres paquetages) ainsi que des bibliothèques Ada telles que Ada.Text_IO
ou Ada.Strings.Unbounded
.
Sur ce diagramme, la flèche blanche ("d'héritage") représentent l'instanciation d'un paquetage : le paquetage dossier est une instanciation du paquetage arbre. Les flèches noires représentent le fait qu'un paquetage dépende un autre, par exemple, le paquetage dossier dépend du paquetage fichier.
Durant l’entièreté du projet, l'objectif a été de faire en sorte que chaque paquetage dépende d'un nombre minimal de paquetage, afin de favoriser la modularité.
Il est à noter que nous avons décidé de coder intégralement en anglais. Nous pensons que c'est une bonne pratique et qu'il est important de s'y habituer. Cependant, nous n'avons pas eu le temps de traduire nos raffinages, nous les avons donc laissé en français.
À plusieurs occasions dans le projet, nous avons utilisé la commande subtype
afin de renommer des types de données avec un alias plus court. Nous avons trouvé cette pratique dans la documentation fournie sur le site https://docs.adacore.com.
Le type Unbounded_String
de la bibliothèque Ada.Strings.Unbounded
a été utilisé, préférablement au type String
. Le type Unbounded_String
permet de définir et d'utiliser des chaînes de taille variable.
Un nombre important de choix plus ou moins arbitraires a été fait. Pour chaque paquetage, il convient de détailler ceux-ci :
P_Constants
:- Une taille maximum de
100
a été définie pour les tableaux. Comme on le verra plus tard, les dossiers et les fichiers sont contenus dans des tableaux. Ces derniers ne pourront donc pas excéder100
fichiers et100
dossiers. - La taille maximum d'une
String
a été définie à100
caractères, pour tout notre projet. Cela permet d'endiguer tout potentiel problème de débordement. Étant donné qu'Ada est un langage robuste, il ne permettra pas qu'une chaîne saisie par un utilisateur dépasse cette taille de100
(sinon quoi il la coupera). Il a par ailleurs été décidé de vérifier, dans tout le projet, que la taille desString
fournies en paramètre des sous-programmes ne dépasse jamais cette taille de100
(via des pré-conditions). - La taille maximum d'un fichier a été définie à
1Go
, c'est à dire à1000*1000*1000
octets. Comme nos tailles sont exprimées en octets, une limite plus haute aurait dépassé la valeur maximum d'unInteger
en Ada, nous forçant à utiliser desLong
, ce que nous ne voulions pas. - La taille d'un dossier est de
10Ko
, cela été stipulé dans le sujet. - Une constante
FILE_SEPARATOR
a été définie, elle est égale à un/
. La raison derrière ce choix est que si nous décidons de changer le caractère séparateur de notre SGF, nous n'aurons qu'à changer une fois cette constante, et tout le reste du programme s'adaptera.
- Une taille maximum de
P_Array
:- Une structure de données de type tableau a été choisie. Cela peut paraître désavantageux, notamment de part le fait qu'un tableau possède une taille fixe et que la mémoire (aussi bien au sens de la RAM que de la mémoire dans la partie 2 du sujet) est limitée. Cependant, plusieurs raisons nous ont poussées vers ce choix :
- Pour accéder à une valeur donnée d'une liste, il faut pouvoir comparer les éléments de cette liste. Or, dans le cas d'un arbre N-aire, nos éléments sont des arbres (c'est à dire des fils d'un nœud). Cependant, le paquetage
P_Tree
est générique. Il n'est pas possible de comparer des éléments génériques sans avoir à spécifier une fonction de comparaison lors de l'instanciation du paquetage. Dans notre cas, cette dite fonction devrait pouvoir prendre deux dossiers en entrée, puis renvoyer vrai si ils sont identiques ou faux sinon. Cependant, nous ne pouvions pas développer de méthode utilisant le type dossier (P_Folder_Tree.T_Tree
) avant d'avoir défini ce même type, type qui n'est défini qu'une fois l'instanciation du paquetageP_Tree
effectuée. Nous étions donc sur un Ouroboros. Si nous avions utilisé les fonctionnalités objet d'Ada, ce problème ne se serait pas présenté. Il était cependant spécifié dans les consignes de ne pas le faire. Ce problème sera détaillé dans les difficultés rencontrées. - Dans Linux, la structure de données utilisée pour représenter les fils d'un dossier est une version très modifiée d'un tableau. En effet, Linux possède un algorithme qui crée des tableaux d'une taille
2^n
, et peut en recréer dynamiquement en fonction de la quantité de données mises dans un dossier. Cependant, en creusant plus, il serait en fait plus exact de parler de chaînes de tableaux, c'est pourquoi nous pensons que notre solution n'est pas optimale. Elle était surtout beaucoup plus pratique pour nous.
- Pour accéder à une valeur donnée d'une liste, il faut pouvoir comparer les éléments de cette liste. Or, dans le cas d'un arbre N-aire, nos éléments sont des arbres (c'est à dire des fils d'un nœud). Cependant, le paquetage
- Nous avons décidé de suivre une programmation offensive, c'est à dire d'utiliser des Pré et Post conditions dans les cas de dépassement de la taille d'un tableau ou de tentative d'accès à une case inexistante du tableau. Étant donné que c'est nous qui gérons ce type de problème plus loin dans le code, et que cela ne dépend pas d'une interaction utilisateur, le fait de définir des exceptions aurait été de trop.
- Afin de respecter le principe d'encapsulation, le type
T_Array
a été défini en privé. - Il était important pour nous que ce paquetage soit générique car nous l'utilisons plusieurs fois dans le programme (tableau de dossiers, tableau de fichiers, tableau de chaînes, etc...).
- Une structure de données de type tableau a été choisie. Cela peut paraître désavantageux, notamment de part le fait qu'un tableau possède une taille fixe et que la mémoire (aussi bien au sens de la RAM que de la mémoire dans la partie 2 du sujet) est limitée. Cependant, plusieurs raisons nous ont poussées vers ce choix :
P_Tree
:- Étant donné que ce paquetage sera par la suite instancié en tant que dossier, il n'était pas envisageable d'utiliser un arbre binaire (tel qu'étudié lors des cours de "Méthodologie de la Programmation"). Il était donc naturel d'imaginer un arbre N-aire.
- Afin de respecter au mieux l'encapsulation, le type
T_Node
, qui représente un nœud de l'arbre, a été défini en privé. Cette nuance est importante puisqu'elle a, par la suite, guidé une partie des choix. - Nous avons décidé que les fils d'un nœud seraient représentés sous forme de tableau.
P_Metadata
:- Afin de respecter l'encapsulation, le type
T_Metadata
a été défini en privé. Cependant, les typesE_Rights
etT_Rights
permettant le représentation des droits utilisateurs ont été définis en public. Nous souhaitions qu'un utilisateur puisse créer lui même un droit, et le passer en paramètre à des fichiers et dossiers.
- Afin de respecter l'encapsulation, le type
P_File
:- Afin de simplifier notre algorithmie, il a été décidé qu'un fichier (
T_File
) soit un pointeur. Grâce à cela, un fichier peut prendre la valeur nulle. Un fichier est donc un pointeur sur une structure de données de type enregistrement (T_R_File
). - Une fois crée, il n'est pas possible de modifier directement le chemin d'accès d'un fichier. Cela laisserait une porte ouverte à de nombreuses incohérences. Une fonction
clone
a donc été développée, elle renvoie le clone d'un fichier, moyennant une modification de son chemin d'accès (et, potentiellement, de son nom). Il s'agit d'une modification du patron de conception "Prototype". - Afin de respecter au mieux l'encapsulation,
T_R_File
a été défini en privé.
- Afin de simplifier notre algorithmie, il a été décidé qu'un fichier (
P_Folder
:- Étant donné que le type dossier vient en fait du paquetage
P_Folder_Tree
qui est une instanciation deP_Tree
, le moyen pour y accéder étaitP_Folder_Tree.P_Tree
, nous l'avons renommé enP_Folder
à l'aide desubtype
. - Un dossier contient des dossiers et des fichiers.
T_Folder
étant unT_Tree
instancié, les dossiers sont donc les enfants dans l'arbre. Cependant, il a fallu trouver un moyen d'ajouter des fichiers enfants d'un dossier à un dossier, et ce, sans modifierP_Tree
. Pour cela, nous avons utilisé une instanciation du paquetageT_Array
que nous avons appeléT_Files
. Chaque dossier aura unT_Files
dans ses données. Le dossier a donc deux sources de données : les sous-dossiers qui viennent des enfants dans l'arbre, et les fichiers qui viennent d'un tableau de fichiers dans ses données. - Étant donné qu'un dossier
root
est unique pour toute la durée de vie d'un programme, un patron "Singleton" a été utilisé.root
est en fait crée par une fonction privéecreate_root
, le reste du programme fera appel àroot
via la fonctionget_root
, qui créeraroot
s'il n'existe pas déjà, puis le renverra. - Il n'est pas possible de modifier directement ni le chemin d'accès, ni le parent d'un dossier. Pour "déplacer" un dossier, il faudra en créer un nouveau et copier les données du précédent vers le nouveau, à la manière du clonage dans
P_Files
. - Le type
T_Folder_Data
, qui contient les métadonnées (T_Metadata
) et le tableau de fichiers (T_Files
) d'un dossier doit être en public, sans quoi nous ne pouvons pas instancierP_Tree
avec.
- Étant donné que le type dossier vient en fait du paquetage
P_Substrings
:- Afin de manipuler des chaînes de taille variable, il a fallu utiliser le paquetage
Ada.Strings.Unbounded
d'Ada. - Le type
T_Substrings
est public car c'est une instanciation deP_Array.T_Array
contenant desUnbounded_String
et que nous souhaitons que le reste du programme sache que c'est un tableau. De plus, le fait d'utiliser la commandesubtype
pour renommerP_Substrings_Array.T_Array
enT_Substrings
nous force à mettreT_Substrings
en public car Ada ne nous permet pas de déclarer dessubtype
privés.
- Afin de manipuler des chaînes de taille variable, il a fallu utiliser le paquetage
P_Commands
:- Afin d'afficher les fichiers et dossiers de manière triée par ordre alphabétique, il nous a fallu définir un type
T_R_Sibling
et un typeT_Siblings_Set
dont nous détaillerons l'utilisation dans la prochaine partie. Nous avons ici jugé intéressant d'utiliser le paquetageAda.Containers.Ordered_Sets
et de vous montrer une manière peu commune mais élégante de trier les dossiers et les fichiers. - Dans ce paquetage, la seule procédure publique est
run_command
. Cette procédure permet, via un dossier courant et une ligne de commande, d'exécuter la commande associée ou de renvoyer une erreur. Nous nous sommes rendu compte que seule cette procédure était appelée depuis l'extérieur du paquetage. Nous avons donc décidé de mettre les commandes telles quels
etcd
en privé. Ce choix n'est pas anodin : nous souhaitons cacher au maximum l'implémentation des commandes, afin de créer une sorte de "boîte noire" tel que cela est fait dans la réalité. Cela a rendu extrêmement difficile l'écriture de tests pour les commandes. Puisque Ada ne permet pas de tester des sous-programmes privés, nous pouvions uniquement testerrun_command
en changeant la ligne de commande à exécuter. Nous nous reposons donc sur le fait querun_command
fonctionne correctement afin de tester nos commandes (ce qui n'est pas dangereux étant donné son algorithmie très simple). Nous avons tout de même réussi à écrire des tests poussés pour tester chaque commande individuellement. Cela est une pure limite de la technologie utilisée et nous n'aurions pas fait comme ça dans un autre langage.
- Afin d'afficher les fichiers et dossiers de manière triée par ordre alphabétique, il nous a fallu définir un type
Pour les programmes principaux :
menu
:- Afin d'alléger le code (qui était déjà volumineux avec les affichages !), nous avons décidé de faire appel à
run_command
depuis lemenu
. Le menu récupère donc les choix de l’utilisateur (commande à exécuter, chemin d'accès du fichier à créer, etc...), construit uneString
avec ces choix, puis l'exécute viarun_command
. L'utilisation du parser depuis un menu peut paraître bizarre, cependant, ce procédé est souvent utilisé dans le développement d'applications graphiques qui se basent sur des utilitaires en ligne de commande (tel queGparted
avecParted
). On parle alors de patron "Modèle / Vue / Contrôleur". Ici, le modèle serait notre paquetageP_Commands
et son interface d'accès la procédurerun_command
.
- Afin d'alléger le code (qui était déjà volumineux avec les affichages !), nous avons décidé de faire appel à
terminal
:- Le code du terminal est très succinct, nous avons décidé d'ajouter de la couleur à nos affichages (via les caractères d'échappement ASCII) pour rendre cela plus attrayant.
Enfin, il est primordial de noter que nous avions pour objectif de rendre notre programme robuste. Nous avons ainsi envisagé TOUS LES CAS possibles qui pourraient mettre en défaut un programme développé rapidement. Par exemple, la suppression d'un dossier alors que l'on est dans ce dernier, ou la copie d'un dossier dans un de ses sous-dossiers. Cela a rendu plus pénible notre travail et notre code, mais nous ne pouvions pas consciemment laisser passer des erreurs de la sorte. Notre code est certainement plus lourd que d'autres, car nous gérons beaucoup plus de cas d'erreurs et qu'il est quasiment impossible de factoriser cela. Nous espérons que cet effort sera prit en compte.
P_Array
:- Le type
T_Values
est un tableau d'éléments génériquesT
de tailleNMAX_VALUES
(constante du paquetageP_Constants
). Il représente les valeurs effectives d'un tableau. - Le type
T_Array
est un enregistrement contenant un typeT_Values
(valeurs effectives) et unInteger
(nombre de valeurs effectives).
- Le type
P_Tree
:- Le type
T_Tree
est un pointeur versT_Node
(similaire à ce qui a été fait en cours). - Le type
T_Siblings
est un renommage du typeP_Siblings.T_Array
,P_Siblings
étant une instanciation du paquetageP_Array
. Ce type permet de représenter les arbres enfants d'un arbre à l'aide d'un tableau. L'élément génériqueT
est ici remplacé parT_Tree
. - Le type
T_Node
représente le nœud d'un arbre et est un enregistrement qui contient un élément génériqueT
, un parent de typeT_Tree
et une liste d'enfants de typeT_Siblings
.
- Le type
P_Metadata
:- Le type
E_Rights
est une énumération de droits. Il représente les trois droits de lecture (R
), d'écriture (W
) et d'exécution (X
) ainsi que toutes les combinaisons possibles de ces derniers (RWX
,RX
,RW
,WX
etNONE
). - Le type
T_Rights
est un tableau de troisE_Rights
. Il permet de représenter les droits d'un utilisateur, de son groupe, et des autres utilisateurs. - Le type
T_Metadata
est un enregistrement contenant un nom de typeUnbounded_String
, des droits de typeT_Rights
, une taille de typeInteger
et un chemin d'accès de typeUnbounded_String
. Il permet de représenter les métadonnées d'un dossier ou d'un fichier.
- Le type
P_File
:- Le type
T_R_File
est un enregistrement contenant des métadonnées de typeT_Metadata
et des données de typeString
(tel que cela était suggéré dans le sujet). - Le type
T_File
est un pointeur vers un typeT_R_File
.
- Le type
P_Folder
:- Le type
T_Files
est un renommage du typeP_Files.T_Array
,P_Files
étant une instanciation du paquetageP_Array
. Ce type permet de représenter les fichiers enfants d'un dossier à l'aide d'un tableau. L'élément génériqueT
est ici remplacé parT_File
. - Le type
T_Folder_Data
est un enregistrement contenant des métadonnées de typeT_Metadata
et un tableau de fichiers de typeT_Files
. L'intérêt de ce type est de remplacer l'élément génériqueT
lors de l'instanciation du paquetageP_Tree
. - Le type
T_Folder
est un renommage du typeP_Folder_Tree.T_Tree
,P_Folder_Tree
étant une instanciation du paquetageP_Tree
. Ce type permet de représenter un dossier à l'aide d'un arbre. L'élément génériqueT
est ici remplacé parT_Folder_Data
.
- Le type
P_Substrings
:- Le type
T_Substrings
est un renommage du typeP_Substrings_Array.T_Array
,P_Substrings_Array
étant une instanciation du paquetageP_Array
. Ce type permet de représenter les différentes parties d'une chaîne coupée à l'aide d'un tableau. L'élément génériqueT
est ici remplacé parUnbounded_String
.
- Le type
P_Commands
:- Le type
E_Encoded_Commands
est une énumération des différentes commandes disponibles pour l'utilisateur, à savoirls
,rm
,pwd
,cd
,mkdir
,cp
,mv
,tar
,touch
,help
, etclear
. - Le type
T_R_Sibling
est un enregistrement contenant un nom de typeUnbounded_String
et le fait qu'il s'agisse d'un dossier ou non, représenté par unBoolean
. Ce type permet de représenter le fils d'un dossier, qu'il soit un dossier ou un fichier. L'intérêt de ce type est d'être utilisé lors de l'instanciation du paquetageAda.Containers.Ordered_Sets
. Cela servira afin de trier alphabétiquement les fils d'un dossier lors de leur affichage à l'aide de la commandels
. - Le type
T_Siblings_Set
est un renommage du typeP_Siblings_Set.Set
,P_Siblings_Set
étant une instanciation du paquetageAda.Containers.Ordered_Sets
. Le paquetage est ici instancié à l'aide d'un typeT_R_Sibling
permettant de remplacer l'élément génériqueElement_Type
, et d'une fonctioncompare_T_R_Siblings
permettant de comparer deuxT_R_Sibling
.
- Le type
De nombreux algorithmes ont été développés au cours de ce projet. Dans cette partie, nous allons aborder les principaux.
-
P_Array
:-
del_value
:Le premier algorithme auquel nous avons dû penser suite à notre choix de représenter les sous-dossiers et fichiers d'un dossier comme des tableaux de ces éléments, est l'algorithme de compactage suite à la suppression d'un élément.
index := 1; while index <= get_nb_values(f_array) and then f_array.values(index) /= value loop index := index + 1; end loop; -- if the value is found if index <= get_nb_values(f_array) then -- compact the array while index < get_nb_values(f_array) loop f_array.values(index) := f_array.values(index + 1); index := index + 1; end loop; -- delete an element f_array.nb_values := f_array.nb_values - 1; end if;
Dans cet algorithme, la première partie se consacre à la recherche de la valeur que nous devons supprimer.
On commence par le début du tableau (index = 1), et on poursuit tant que l'on ne trouve pas la valeur recherchée et que la fin du tableau est atteinte, en passant à l'index suivant.
index := 1; while index <= get_nb_values(f_array) and then f_array.values(index) /= value loop index := index + 1; end loop;
Lorsque le parcours est terminé, on regarde la valeur de l'index que l'on a obtenu. Si cette valeur d'index appartient au tableau (c'est à dire que l'index est inférieur à l'index du dernier élément), alors cela veut dire que la valeur à supprimer a été trouvée.
Donc, à partir de cette valeur, nous allons recopier la valeur de la case suivante dans la case courante, et se déplacer ainsi jusqu'à la fin du tableau. De ce fait, le tableau ne contient pas de trou.
if index <= get_nb_values(f_array) then -- compact the array while index < get_nb_values(f_array) loop f_array.values(index) := f_array.values(index + 1); index := index + 1; end loop; -- delete an element f_array.nb_values := f_array.nb_values - 1; end if;
Cet algorithme peut marche car, dans un premier temps, quand le tableau est vide, nous insérons la première valeur en case 1. Ensuite, nous gardons en mémoire dans ce tableau le nombre d'élément inséré, et nous insérons à chaque fois dans la case après l'index du dernier élément inséré. De plus, étant donné que nous utilisons un algorithme de compactage, nous savons que notre tableau ne contient pas d'espace vide.
-
calculate_path
:Un autre algorithme important de ce paquetage est celui de
calculate_path
, qui permet de calculer le chemin d'un dossier, et donc de connaître ses ancêtres directs. Il faut savoir que le chemin va du dossier originel jusqu'au dernier parent du dossier, en ne passant que par les ancêtres directs.Dans un premier temps, si nous voyons que le parent du dossier est le dossier originel, alors nous renvoyons simplement
/
, ce qui représente de dossier originel.Ensuite, si ce n'est pas le cas, le chemin est initialisé avec une chaîne vide, puis nous parcourrons tous les dossier parents, en vérifiant que nous ne sommes pas le dossier originel et que notre parent n'est pas le dossier originel.
À chaque dossier parent parcouru, nous rajoutons son nom en début de chemin.
Nous renvoyons ensuite le résultat obtenu.
if is_root(get_parent(folder)) then return ""&FILE_SEPARATOR; end if; -- initialize absolute path and current folder absolute_path := To_Unbounded_String(""); current := folder; -- while the current parent is not null and the current parent is not root while not is_null(get_parent(current)) and then not is_root(get_parent(current)) loop -- current becomes its parent current := get_parent(current); -- add "/" + name of current at the beggining of absolute path absolute_path := FILE_SEPARATOR & get_name(current) & absolute_path; end loop; -- return absolute calculated path return To_String(absolute_path);
-
find_folder
:L'algorithme de
find_folder
est simple mais a besoin d'être explicité, car c'est sur lui que nous basons tous nos déplacements dans un dossier descendant ou parent, il représente le déplacement élémentaire, qui doit être appelé pour chaque élément d'un chemin de déplacement.Dans notre implémentation, nous avons fait le choix que
./
ou.
représentent le dossier courant, et../
ou..
représentent le dossier parent direct. Donc, lorsque nous voyons que l'élément passé en paramètre est.
, nous renvoyons le dossier courant, et lorsque c'est..
, nous renvoyons le dossier parent, excepté lorsqu'il s'agit du dossier originel, auquel cas on le renvoie, car on ne peut remonter au delà du dossier originel.Ensuite, nous utilisons le même algorithme pour chercher un fichier ou un sous-dossier direct du dossier courant, à la différence près du tableau dans lequel on recherche.
Si le nom reçu en paramètre n'est pas un de ces deux éléments (
.
ou..
), alors nous parcourons tous les sous-dossiers directs, et nous vérifions si le sous-dossier que nous avons a le même nom que celui passé en paramètre.Si c'est le cas, nous retournons le dossier.
Si nous avons fini notre parcours sans trouver de dossier correspondant, nous renvoyons
null
pour le signifier.if folder_name = "." then return current_folder; end if; -- if the folder name is ".." if folder_name = ".." then -- if the current folder is root, return itself if is_root(current_folder) then return current_folder; -- else, return the folder parent else return get_parent(current_folder); end if; end if; -- for all sub-folders for i in 1..get_nb_folders(current_folder) loop -- if the sub-folder name is equal to the given folder_name if get_name ( get_folder(current_folder, i) ) = folder_name then -- return the sub-folder return get_folder(current_folder, i); end if; end loop; -- anyway, return null return null;
-
-
P_Substrings
:-
split_string
:Introduction :
L'algorithme qui nous permet de décoder les commandes que rentre l'utilisateur est celui qui sépare les
String
en fonction d'unCharacter
, tous deux passés en paramètres.Nous avons choisi de stocker chaque partie obtenue dans une
Unbounded_String
, car les tailles sont variables, et nous voulions éviter d'avoir les soucis de taille causés par lesString
Ada.Ensuite, nous stockons le résultat général des différentes parties dans un tableau les regroupant. Ainsi, il suffit d'itérer dessus afin de parcourir le résultat obtenu.
Explication :
Dans un premier temps, si nous voyons que le
String
passé en paramètre est vide, alors nous retournons un tableau vide.Ensuite, si ce n'est pas le cas, on initialise l'index de parcours (
index
) à1
. On utilise également un autre index (index_substring_first
) qui représente l'index du premier élément après le dernier séparateur. Il est initialisé à1
, car c'est lui qui nous permettra de définir le début duUnbounded_String
que nous enregistrerons dans le tableau.On parcours ensuite la
String
en entier.Si on passe sur un caractère qui est le séparateur, on ajoute tous les caractère parcourus dans le tableau, après avoir vérifié que nous avons bien fait un déplacement.
Si nous avons un enchaînement de plusieurs séparateur, nous nous déplaçons de la longueur de cet enchaînement.
Nous nous trouvons alors sur une nouvelle partie de la chaîne originale, après un ou plusieurs séparateurs. Nous enregistrons donc cette position, qui nous servira à savoir quand commence la prochaine partie à enregistrer.
Un fois tous les séparateurs passés, nous passons au caractère suivant, et nous répétons cette boucle.
Ensuite, si la fin de la chaîne est atteinte et que le dernier caractère n'est pas un séparateur, il nous faut quand même enregistrer cette dernière partie, puisque cet enregistrement dans la boucle ne se faisait que lors d'un passage sur un séparateur.
Nous retournons finalement le tableau obtenu.
substrings := P_Substrings_Array.create; -- if the original string is blank, return an empty array of substrings if original = "" then return substrings; end if; index := 1; index_substring_first := 1; -- for all the original string while index <= original'Length loop -- if the current character is the separator if original(index) = separator then -- check if we really need to add string, because original(1..0) will be add, and we don't want this if index_substring_first <= (index - 1) then -- add the substring add_substring(substrings, original(index_substring_first..(index - 1))); end if; -- while the separator is encountered, skip it while index < original'Length and then original(index + 1) = separator loop index := index + 1; end loop; index_substring_first := index + 1; end if; index := index + 1; end loop; -- if the last character is not a separator if original(original'Last) /= separator then -- add the last substring add_substring(substrings, original(index_substring_first..original'Last)); end if; return substrings;
-
-
P_Commands
:-
get_options_and_parameters
:Introduction :
Cette fonction sépare les différents paramètres et options qui sont présentes dans les commandes saisies par l'utilisateur.
Les arguments de la fonction sont sous forme d'un tableau d'
Unbounded_String
.Explication :
On parcourt tous les arguments passés en paramètres.
Si on détecte que l'argument parcouru commence par
-
, cela veut dire que c'est une option. Si il n'y a que ce caractère, alors on lève une erreur comme quoi l'option est vide. Ensuite, nous parcourons tous les caractères après ce-
de l'argument actuel, et nous enregistrons chaque caractère comme option.Si ce n'est pas une option, alors nous enregistrons l'argument comme paramètre.
for i in 1..get_nb_substrings(arguments) loop -- If it starts with a "-", then it's an option if get_substring_to_string(arguments, i)(1) = '-' then -- If we only have "-", we raise an error if get_substring_to_string(arguments, i)'Length < 2 then raise Empty_Option_Error; end if; -- We start at 2 because we need to get all options, and not the "-" character for j in 2..get_substring_to_string(arguments, i)'Length loop -- We have to transform it into a string, because the (j) part return a Character, and we add it to the options array add_substring(options, get_substring_to_string(arguments, i)(j) & ""); end loop; else -- Else it's an argument and we add it to the arguments array add_substring(parameters, get_substring_to_string(arguments, i)); end if; end loop;
-
split_options
:Introduction :
Cette fonction est nécessaire pour séparer les options qui sont données en paramètre sous forme de
String
(-rf
ou-r -f
). Elle est utilisé pour lever les exceptions quant aux commandes non supportées dans les procédures des commandes.Explication :
On crée un tableau qui recevra les options. On sépare alors la chaîne de caractère en fonction du séparateur
-
, et on stock le résultat dans un tableau d'Unbounded_String
. On parcourt toutes les les parties obtenues. Si pour une partie, on remarque que la longueur de la chaîne est supérieure à1
, cela veut dire que cette chaîne comporte plusieurs options, et on les ajoute alors une à une au tableau d'options. Sinon, il n'y a qu'une seule option dans la partie, et on l'ajoute au tableau d'options.On retourne alors le tableau d'options.
options := create_substrings; splitted_option := split_string(options_as_string, '-'); for i in 1.. get_nb_substrings(splitted_option) loop -- If there is multiple option wrote after a "-" (like "-rf") if get_substring_to_string(splitted_option, i)'Length > 1 then -- We add each one of them to the options for j in 1.. get_substring_to_string(splitted_option, i)'Length loop add_substring(options, ""&get_substring_to_string(splitted_option, i)(j)); end loop; -- Else, It's a simple option like "-r" else -- we add the option to the options add_substring(options, ""&get_substring_to_string(splitted_option, i)(1)); end if; end loop; return options;
-
only_handled_options
:Introduction :
Cette fonction sert à vérifier que les options passées en paramètres sous forme de tableau sont bien contenues dans les options passées sous forme de chaîne de caractère.
Explication :
On récupère les options passées sous forme de chaîne de caractère, que l'on stock dans un tableau comme le tableau des options supportées.
On prend ensuite chaque option des options passées en paramètres sous forme de tableau, et on vérifie que chacune d'entre elle appartient au tableau des options supportées. On retourne faux sinon.
On retourne vrai à la fin, si aucune option n'a retourné faux avant.
splitted_handled_options := split_options(handled_options); for i in 1..get_nb_substrings(options) loop -- return False if a provided option is not supported if not contains_option(splitted_handled_options, get_substring_to_string(options, i)(1)) then return False; end if; end loop; -- return True anyway return True;
-
go_to_folder
:Introduction : Cette fonction est celle qui nous permet de nous déplacer le long de l'arbre des dossiers. Elle est donc essentielle afin de pouvoir mettre en place une arborescence, s'y déplacer, et utiliser les commandes en son sein.
Le principe est simple. Depuis un chemin et un dossier, on essaye de passer à l'élément suivant du chemin, et ce jusqu'au bout de celui-ci, tout en mettant à jour à chaque déplacement le dossier depuis lequel on tente le déplacement suivant.
Explication : Si le chemin est vide, alors on indique que l'on ne peut trouver le dossier et on lève une exception.
Si le premier élément du chemin est
/
, le début de notre chemin, et donc le dossier courant du parcours, sera le dossier originel, sinon cela sera le dossier passé en paramètre.Ensuite, nous récupérons depuis les paramètres si on doit s'arrêter à l'avant dernier élément du chemin. On sépare alors notre chemin, en séparant en fonction du caractère
/
.On parcourt alors tous les éléments de notre séparation, jusqu'à l'avant-dernier ou le dernier, en fonction du paramètre
stop_at_penultimate
.Pour chaque élément obtenu et parcouru, on cherche à l'atteindre directement depuis le dossier courant. Si ce n'est pas possible, alors on lève une exception, sinon on fait le déplacement en changeant le dossier courant.
Quand tous les déplacements sont faits, on retourne le dossier trouvé.
if path'Length <= 0 then raise Invalid_Folder_Error; end if; -- if this is an absolute path if path(path'First) = FILE_SEPARATOR then -- start from root current := get_root; else -- else, start from the current folder current := original_directory; end if; -- we might want to stop at the penultimate of the given path, when using mkdir for example penultimate := (if stop_at_penultimate then 1 else 0); -- follow the path from the defined start path_tree := split_string(path, FILE_SEPARATOR); for i in 1..get_nb_substrings(path_tree) - penultimate loop -- if the following directory doesn't exist, the path is invalid, raise an exception if is_null(find_folder(current, get_substring_to_string(path_tree, i))) then raise Invalid_Folder_Error; end if; -- take the next sibling as current current := find_folder(current, get_substring_to_string(path_tree, i)); end loop; return current;
Cet algorithme est basé sur la recherche de parent et enfant direct cachée dans
find_folder
. -
folder_deep_copy
:Introduction :
Ceci est un algorithmes récursif. Il a pour but de copier toutes les données et sous dossier d'un dossier donné dans un autre.
Il créé pour cela une copie des anciens fichiers, et des dossier et de tous les sous-dossiers directs ou non directs, ainsi que leurs fichiers.
Explication :
On parcourt chaque fichier du dossier à copier, et on insert chaque copie dans le dossier recevant les copies.
Ensuite, on parcourt chaque sous-dossier direct du dossier original. Pour chaque sous-dossier direct, on crée une copie dans le dossier recevant la copie, puis on réappelle
folder_deep_copy
avec en dossier source le sous-dossier direct que le vient de copier depuis le dossier original, et en dossier de destination la copie que l'on vient de créer.De cette manière, tous les sous-dossier non directs sont copiés également.
for i in 1..get_nb_files(source_folder) loop original_file := get_file(source_folder, i); new_file := clone(original_file, get_pwd(destination_folder)); add_file(destination_folder, new_file); end loop; -- copy all folders from original folder for i in 1..get_nb_folders(source_folder) loop original_folder := get_folder(source_folder, i); new_folder := create(get_name(original_folder), destination_folder, get_rights(original_folder)); folder_deep_copy(get_folder(source_folder, i), new_folder); end loop;
-
calculate_size
:Introduction :
Cet algorithme est récursif. Il sert à calculer la taille du dossier, en additionnant la taille du dossier actuel, de tous ses fichiers, et de tous ses sous-dossier ainsi que leurs fichiers et sous-dossier, en se propageant jusqu'aux derniers descendants.
Explication :
Nous prenons la taille du dossier actuelle, à laquelle on ajoute la taille de tous les fichiers du dossier.
On parcourt ensuite chaque sous-dossier direct en ajoutant sa taille calculée avec
calculate_size
.De ce fait, on ajoute toutes les tailles des descendants.
current_folder_size := FOLDER_SIZE; -- + files size for i in 1..get_nb_files(current_directory) loop current_folder_size := current_folder_size + get_size(get_file(current_directory, i)); end loop; -- + subfolders size for i in 1..get_nb_folders(current_directory) loop current_folder_size := current_folder_size + calculate_size(get_folder(current_directory, i)); end loop; return current_folder_size;
-
create_siblings_set
:Introduction :
Cette fonction sert à enregistrer tous les noms des fichiers et sous-dossiers directs d'un dossier, tout en gardant l'information si le nom correspond à un nom de dossier ou non.
Explication :
On parcourt chaque dossier, et on insère son nom dans l'ensemble des nom en précisant que c'est un nom de dossier.
On parcourt chaque fichier, et on insère son nom dans l'ensemble des nom en précisant que ce n'est pas un nom de dossier.
On retourne l'ensemble contenant tous les noms.
for i in 1.. get_nb_folders(current_directory) loop new_element.name := To_Unbounded_String(get_name(get_folder(current_directory, i))); new_element.is_folder := True; siblings_set.insert(new_element); end loop; -- add each file for i in 1.. get_nb_files(current_directory) loop new_element.name := To_Unbounded_String(get_name(get_file(current_directory, i))); new_element.is_folder := False; siblings_set.insert(new_element); end loop; return siblings_set;
-
Étant donné que nous possédions un bagage en tests unitaires, nous avons tenté de reproduire ce concept. Nous ne savions pas si Ada proposait des outils permettant de faire des tests unitaires comme cela peut exister avec JUnit. Ainsi, nous avons décidé de tester chaque sous-programme de chaque paquetage, un à un, ainsi que les potentielles levées d'exceptions de ces derniers.
Les tests nous ont servi en deux temps : premièrement, ils nous ont permis de savoir si les sous-programmes développés fonctionnaient tel que nous l'attendions. Deuxièmement, nous les avons utilisé comme tests de non-régression afin de s'assurer que la refactorisation de nos codes n'entraînait pas de nouvelles erreurs. Chaque test a été développé le plus tôt possible, quand un paquetage était terminé.
Nous n'avons pas adopté une démarche de travail de type TDD qui consiste à développer les tests en amont du programme lui-même. En effet, n'ayant pas eu de cours de TDD, nous avons préféré faire avec ce que nous connaissions.
Il est important de noter que, étant donné l'ampleur des commandes, un paquetage de test a été défini pour chacune d'elles. Ils portent des noms tels que test_cp.adb
.
Ceci est la table d'équivalence permettant d'expliquer quel test porte sur quel paquetage / commande.
Paquetage / Commande testée | Programme de test associé |
---|---|
P_Array |
test_array.adb |
P_File |
test_file.adb |
P_Folder |
test_folder.adb |
P_Metadata |
test_metadata.adb |
P_Substrings |
test_substrings.adb |
P_Tree |
test_tree.adb |
cd |
test_cd.adb |
cp |
test_cp.adb |
ls |
test_ls.adb |
mkdir |
test_mkdir.adb |
mv |
test_mv.adb |
pwd |
test_pwd.adb |
rm |
test_rm.adb |
tar |
test_tar.adb |
touch |
test_touch.adb |
Pour les tests, chaque ligne de succès est affichée en vert et chaque ligne d'échec est affichée en rouge. Les lignes blanches représentent des informations.
Nous allons rapidement présenter comment nous avons testé un sous-programme et comment nous avons testé une levée d'exception, de façon sommaire.
Test d'un sous-programme, exemple de la fonction get_pwd
du paquetage P_Folder
:
-- get pwd
put_line("Get pwd:");
if get_pwd(folder_sibling) = "/project_old/drafts" then
put_line("get_pwd(folder_sibling) = ""/project_old/drafts""");
else
put_line("get_pwd(folder_sibling) is incoherent");
end if;
new_line;
Ici, un message affiche quel sous-programme va être testé. Nous effectuons ensuite une vérification, nous savons que le pwd
de ce dossier se doit d'être /project_old/drafts
puisque nous l'avons crée de la sorte. Si la condition est vérifiée, un message de succès est affiché, sinon, un message suivi de is incoherent
est affiché.
Test d'une levée d'exception, exemple de la fonction add_file
du paquetage P_Folder
lors de l'ajout d'un fichier portant le même nom qu'un fichier déjà présent :
-- raising Same_Name_Error when adding a file
put_line("Raising Same_Name_Error when adding a file:");
add_file(folder, create("common_file_name", (RW, R, R), "data"));
begin
add_file(folder, create("common_file_name", (RWX, RX, RX), "executabledata"));
put_line("Nothing raised, add_file should have raised Same_Name_Error");
exception
when Same_Name_Error =>
put_line("Same_Name_Error raised, you can't have two files with the same name in the same folder");
end;
new_line;
Grâce à l'aide d'un bloc begin
, nous affichons un message de succès lorsqu'une exception est levée (via when
). Cependant, si l'exception n'a pas été levée, alors le code s'exécutera linéairement et un message d'échec sera affiché.
Voici une liste (non-exhaustive) des difficultés rencontrées au fil du projet :
- Le fait que le type
String
soit contraint à des tailles fixées en Ada a rendu le travail avec des chaînes de caractères difficile. - L'absence de framework adapté pour les tests (par exemple, JUnit en Java) a rendu difficile la programmation de ces derniers.
- Le développement des tests liés aux commandes (
cp
,ls
, etc...) était ardu étant donné que, à cause d'un choix de conception, nous avions décidé de rendre ces dernières privées. - Le tri des dossiers et fichiers par ordre alphabétique était plutôt compliqué, d'autant plus que les dossiers et les fichiers ne sont pas de même type.
- L'utilisation d'un type liste chaînée nous posait problème, cela a déjà été longuement détaillé dans la partie "Choix réalisés".
- Le fait que la non-répétitivité du code soit un critère de notation, mais que la programmation objet était interdite a rendu difficile certaines tâches de factorisation.
- Le manque de séances attribuées à un projet de cet envergure fut également un facteur de difficulté. Le fait que nous étions dans une période d'examen plutôt chargée a rendu plus compliqué le travail à domicile, puisque d'autres modules devaient être travaillés.
Voici les solutions respectives adoptées pour chaque problème :
- Le type
Unbounded_String
de la librairieAda.Strings.Unbounded
a été utilisé. - La façon de tester les paquetages a été mûrement réfléchie. La méthode utilisée a été décrite dans la partie "Tests".
- L'utilisation de la procédure
run_command
et le fait de se reposer sur son bon fonctionnement a été décidé. - Un ensemble trié de la librairie
Ada.Containers.Ordered_Sets
a été utilisé. - Un paquetage générique
P_Array
a été développé et utilisé. - Des paquetages génériques ont été développés, leurs instanciations ont été faites avec le mot clé
new
. - Nous avons, avec l'accord des professeurs, travaillé depuis chez nous lorsque des journées entières de TP étaient prévues à l'ENSEEIHT. Cela nous a permis d'être plus efficaces puisque nous avons pu travailler, au calme, sur des ordinateurs de bureau. De plus, il a été décidé de se concentrer sur la première partie du projet, et de ne pas aborder la deuxième. D'autant qu'un de nos professeurs avait expliqué que celle-ci était optionnelle.
La première partie du projet est intégralement terminée. Nous avons implémenté toutes les commandes (et même d'autres !) qui étaient listées dans le sujet.
Par manque de temps, nous ne nous sommes cependant pas penchés sur la deuxième.
Nous avons raffiné, factorisé et commenté nos codes au maximum. Ainsi, nous espérons avoir produit un projet de qualité.
- Le développement de la partie 2 vient évidemment à l'esprit.
- Il pourrait également être intéressant de retravailler les types de données utilisés, notamment l'utilisation d'un tableau comme moyen de stockage.
- Mettre en valeur la réalisation de ce projet, à l'aide par exemple d'un Git public, afin de montrer le fruit de notre travail à de potentiels passionnés.
Nous fûmes dans un premier temps très enjoués par le projet. Nous avons pris beaucoup de plaisir lors de la conception et de l'implémentation. Cependant, au fur et à mesure que le projet avançait, nous envisagions de nombreux cas à traiter. Cela a pu causer une certaine démotivation lors du milieu de l'implémentation. Néanmoins, le fait de se rapprocher de la finalité du projet fur très motivant. Au fil du développement, le fait d'observer le bon fonctionnement de notre projet nous donnait de la satisfaction.
Le sujet en lui-même était intéressant puisqu'il s'agissait d'un SGF, outil que nous utilisons tous les jours. Nous avons découvert comment fonctionnaient un SGF et un système d'exploitation, et nous verrons désormais les SGF différemment. Nous n'aurions sûrement pas été aussi passionnés si le sujet n'était pas technique.
Le projet nous a fortement enthousiasmé, nous y avons environ passé 35h par personne. Ce qui ramène le total à environ 70h.
En ce qui concerne la répartition du temps, voici des pourcentages permettant d'estimer globalement le temps passé sur chaque partie :
- 55% à la conception
- 20% sur les types de données
- 35% sur la conception et le raffinage des algorithmes
- 10% sur les tests
- 25% sur l'implémentation
- 10% sur le rapport
Nous avons essayé de suivre au mieux les enseignements en ne négligeant pas la conception et les raffinages. C'est la raison pour laquelle le temps passé à l'implémentation peut paraître bas.
Plusieurs enseignements sont à tirer de ce projet, et du cours de "Méthodologie de la Programmation" en général. Grâce à ces derniers, nous avons pris conscience de l'importance de la bonne conception d'un programme. Nous avons également découvert la méthode des raffinages, cette dernière nous a permis de gagner un temps précieux lors de l'implémentation de notre projet.
Dans un cadre plus technique, nous avons appris à maîtriser certains aspects du langage Ada, ce qui est toujours intéressant pour notre bagage personnel. Nous nous sommes rendu compte que, même avec des langages de bas niveau, il était possible de développer des projets d'envergure.
Nous ferons de notre mieux afin que la rigueur développée lors de ce projet nous accompagne tout au long de notre apprentissage et de notre carrière professionnelle.