Mis à jour le 30 janvier 2006
TD 1 : Système de fichiers

TD 1 : Système de fichiers

10 janvier 2006


1  Question de cours

Bob et Alice et Robert sont dans le même groupe de TD. Bob veut montrer son programme (composé de plusieurs fichiers de grosses taille) à Alice sans que Robert (qui peut deviner les noms des fichiers) puisse le lire.

Comment peut-il faire simplement en créant des fichiers et des répertoires avec des droits d'accès bien choisis (Bob peut chuchoter une information à Alice sans que Bob l'entende)?

(Indice)
Il le place dans un répertoire avec un nom «biscornu» qu'il communique seulement à Alice. Attention, il doit prendre une précaution, laquelle?
(Solution)
Il crée un répertoire ~/triche qu'il interdit en lecture et place son programme dans un sous-répertoire de ~/triche avec un nom «biscornu» qu'il communique seulement à Alice. Donner la liste des commandes, dans l'ordre, exécutées par Bob avant d'envoyer de chuchoter un secret à Alice. (On pourra utiliser les commandes shell mkdir, chmod, cp.)
(Solution)
      
Bob crée un sous-répertoire de /tmp
# ( tout le monde a le droit d'accès)
mkdir /tmp/triche
Il interdit la lecture sur le répertoire (en retirant le bit r)
mais autorise l'accès aux sous-répertoires de ce répertoire (en ajoutant le
bit x)
chmod og=x /tmp/triche
maintenantil peut créer un sous-répertoire avec un nom difficile
à deviner puis y placer ses fichiers
mkdir /tmp/triche/4748850757602420532
cp *.ml* /tmp/triche/4748850757602420532
il faut donner les droits de lecture au sous-répertoire et à tout son
contenu.
chmod -R og=r /tmp/triche/4748850757602420532
MaintenantBob peut donner le nom du répertoire
# /tmp/triche/4748850757602420532 à Alice.
Remarque
Une autre solution que l'on peut envisager est que Bob crée une copie des fichiers dans un sous-répertoire avec les droits de lectures pour le propriétaire seul puis qu'il donne la propriété du fichier à Alice. Cependant, sur certains système l'opération d'abandon de droits (avec chown) demande d'avoir des privilèges. En fait Bob a fait cd pour se placer dans son répertoire de login ~bob, puis il a fait une fausse manipulation: il a exécuté la commande chmod -x . alors qu'il voulait faire chmod og-x . et s'affole lorsqu'il fait ls -ld et voit le résultat. Essayant de réparer, il fait chmod og+x . mais il panique à la vue du résultat.

Sauriez-vous lui expliquer ce qui s'est passé et l'aider le à réparer?
(Solution)
La commande chmod -x . retirer les droit d'exécution à tout le monde. Pour un répertoire, cela interdit de se déplacer dans les sous-répertoires. La commande ls -ld sans argument droit être comprise comme ls -ld . et comme la commande chmod +x . elle désigne une chemin relatif par rapport au répertoire courant donc de se déplacer dans un sous-répertoire du

2  Mise en jambes

Écrire des commandes mon_true et mon_false comparables aux commandes true et false du système (tester man true et man false) qui ne font rien et dont les valeurs de retour sont respectivement toujours 0 et toujours 1. Pour préciser la valeur de retour, on utilisera la fonction exit du module Pervasives. Ce module est ouvert par défaut, il n'est donc pas nécessaire de préfixer cette fonction par le nom du module, ni d'ouvrir le module (open Pervasives). Pour afficher la valeur de retour d'une commande qui vient d'être exécutée par l'interpréteur de commandes on utilisera la commande echo $?. (Testez vos programmes!)
(corrigé)
Le fichier mon_true.ml contient seulement:
      
exit 0;;
En fait, on pourrait simplement écrire un fichier vide, car le compilateur ajoute automatiquement un appel à exit 0 à la fin du programme qui sera donc exécuté si un autre appel à exit n'est défini pas effectué par l'utilisateur. Le fichier mon_false.ml contient:
      
exit 1;;
Écrire une commande bonjour qui affiche la chaîne de caractères "Bonjour monde !" suivie d'un retour à la ligne sur la sortie standard et qui a toujours pour valeur de retour 0. Pour réaliser l'affichage d'une chaîne de caractères suivie d'un retour à la ligne, on utilisera la fonction print_endline du module Pervasives.
(corrigé)
      
print_endline "Bonjour monde!";;
Écrire une commande mon_echo qui affiche les chaînes de caractères passées en argument sur la sortie standard séparées par un espace (pas d'espace à la fin !) et a pour valeur de retour 0. Les arguments de la ligne de commande sont placés dans le tableau argv du module Sys dont l'élément d'indice i est récupéré par Sys.argv.(i). La longueur d'un tableau est retournée par la fonction Array.length. L'affichage d'une chaîne de caractères sans retour à la ligne se fait par la fonction print_string du module Pervasives.
(corrigé)
      
(* on ignore Sys.argv.(0) qui est le nom de la commande et pas un
argument de la commande *)

if Array.length Sys.argv > 1 then
  begin
    print_string Sys.argv.(1);
    for i = 2 to Array.length Sys.argv - 1 do
      print_char ' ';
      print_string Sys.argv.(i)
    done;
  end;
print_newline ();;

3  L'environnement d'exécution

Écrire une commande mon_printenv qui affiche des informations sur les variables d'environnement et qui a le même comportement que la commande printenv du système (man):

Lorsque la commande est appelée sans argument, elle affiche l'ensemble des variables d'environnement et retourne 0 lorsque tout s'est bien passé. Pour cela, on utilisera la fonction Unix.environment de type unit -> string array qui retourne l'ensemble des variables d'environnement sous la forme d'un tableau et la fonction Array.iter de type ('a -> unit) -> 'a array -> unit qui permet d'itérer une fonction passée en premier argument sur les éléments d'un tableau passé en second argument.

Lorsque la commande est appelée avec au moins un argument, elle affiche (sur des lignes séparées) les valeurs des variables d'environnement dont les noms sont passés en arguments. Si un nom ne correspond pas à une variable d'environnement valide, la commande n'affiche rien pour cet argument, mais essaye d'afficher les valeurs des arguments suivants. Si au moins un des arguments n'est pas le nom d'une variable d'environnement valide la commande retourne 1, sinon elle retourne 0. Pour récupérer la valeur d'une variable d'environnement en fonction de son nom, on utilisera la fonction Sys.getenv. Cette dernière lève une exception Not_found lorsque le nom passé en argument ne correspond pas à une variable d'environnement valide.
(corrigé)
      
match Array.length Sys.argv with
  1 ->
    Array.iter print_endline (Unix.environment ())
n ->
    let errors = ref false in
    for i = 1 to n - 1 do
      try
        print_endline (Unix.getenv Sys.argv.(i))
      with Not_found -> errors := true
    done;
    if !errors then exit 1;;

4  Répertoire de travail courant

Le but de l'exercice est d'écrire une commande mon_pwd qui affiche le répertoire courant équivalente à la commande /bin/pwd du système.

Faire cela en une ligne avec la fonction Unix.getcwd.
(corrigé)
      
Unix.handle_unix_error print_endline (Unix.getcwd());;
On a utilisé ici la fonction Unix.handle_unix_error de type ('a -> 'b) -> 'a -> 'b pour récupérer et afficher les éventuelles formes de l'exception Unix_error levées dans le module Unix lors des appels système.

La commande Unix.getcwd n'est pas toujours directement un appel système mais simplement une fonction écrite en C (qui elle-même fait des appels système). On se propose donc de réécrire la fonction getcwd en OCaml. Cette fonction doit pouvoir être utilisée comme fonction de librairie : elle ne doit donc pas modifier les variables globales du programme ni créer une fuite de mémoire. Cette fonction remonte récursivement dans la hiérarchie jusqu'à la racine du système en recherchant à chaque étape le répertoire courant « . » dans le répertoire supérieur « .. ». Décrire le schéma général du programme.
(corrigé)
Un programme a un répertoire de travail courant (mémorisé dans une variable système cwd) à partir duquel sont résolus les chemins relatifs, mais le chemin absolu de ce répertoire n'est pas mémorisé.

Pour reconstruire ce chemin, on remonte pas à pas dans la hiérarchie. Un noeud sera désigné par un chemin relatif p par rapport au répertoire de travail courant noté n(p). Un noeud n(p) est la racine s'il est égal à son parent n(../p); dans ce cas le chemin cherché est /. Sinon, il suffit de concaténer le chemin menant au parent (appel récursif) avec l'arc menant du parent au fils (qui doit exister, sauf peut-être si la hiérarchie a été modifiée pendant le calcul). Pour trouver celui-ci, il suffit de rechercher parmi les f arcs issus du parent n(../p) un arc menant au fils, c'est-à-dire tel que n(../p/f) et n(p) sont égaux. Écrire une fonction equal_node de type stats -> stats -> bool qui teste si deux noeuds de la hiérarchie de fichiers sont identiques. Deux noeuds sont identiques si et seulement si leurs numéros de noeuds et leurs numéros de partition sont égaux.

(corrigé)
      
open Unix;;

let equal_node n1 n2 =
  n1.st_ino = n2.st_ino && n1.st_dev = n2.st_dev;;
Écrire une commande try_finally qui prend quatre arguments f, x, finally et y et qui effectue le calcul f x, puis, avant de retourner le résultat, exécute finally y, y compris lorsque le résultat est une exception.
(corrigé)
      
let try_finalize f x finally y =
  let res = try f x with exn -> finally yraise exn in
  finally y;
  res;;
Écrire une fonction dir_find qui prend en arguments un prédicat f string -> bool et un nom de répertoire et recherche dans celui-ci le nom d'un fichier qui satisfait le prédicat f. Si la fonction trouve le fichier elle retourne son nom, sinon elle lève l'exception Not_found. Pour écrire cette fonction, on utilisera les fonctions Unix.opendir, Unix.readdir et Unix.closedir. Pour ne pas créer une fuite de mémoire, on fera bien attention à refermer le répertoire ouvert, y compris lorsqu'une exception est levée, avant de rendre le résultat ou de relever une exception.
(corrigé)
      
let dir_find f path =
  let dir_handle = opendir path in
  let rec find () =
    let name = readdir dir_handle in
    if f name then name else find () in
  try
    try_finalize find () closedir dir_handle
  with End_of_file -> raise Not_found;;
Écrire une fonction mon_getcwd qui se comporte comme Unix.getcwd. L'algorithme manipule des chemins et des noeuds. Les informations sur les noeuds, indispensables à la comparaison de noeuds, sont obtenues par l'appel système Unix.lstat qui prend en argument un chemin (on n'utilise pas Unix.stat ici car on recherche un chemin directe issu de la racine qui ne traverse pas de liens symboliques). Pour être portable, on utilisera les fonctions concat, current_dir_name (.) et parent_dir_name (..) du module Filename pour manipuler les chemins. On évitera l'utilisation de chdir qui affecterait alors le reste du programme.
(corrigé)
L'appel système Unix.lstat est coûteux, on évite donc de répéter plusieurs fois. Pour cela, on va maintenir pour chaque noeud une structure qui regroupe le chemin et l'information sur le noeud associé au chemin.
      
type info = { path : stringlstat : stats };;

let info path = { path = pathlstat = lstat path };;
Voici la fonction mon_getcwd, elle peut lever l'exception Not_found lorsqu'on ne réussit pas à trouver de chemin, ce qui peut se produire si l'arborescence a été modifiée en cours de calcul (par un autre programme s'exécutant en parallèle).
      
open Filename;;

let mon_getcwd() =
  let rec find_root node =
    let parent_node = info (concat parent_dir_name node.pathin
    if equal_node node.lstat parent_node.lstat then "/"
    else
      let found name =
        name <> current_dir_name && name <> parent_dir_name &&
        equal_node node.lstat (lstat (concat parent_node.path name)) in
      let name = dir_find found parent_node.path in
      concat (find_root parent_nodename in
  find_root (info current_dir_name);;
Noter qu'il est inutile de tester current_dir_name et parent_dir_name car on sait à coup sûr qu'ils ne peuvent pas convenir. On évite ainsi deux appels systèmes inutiles. Terminer le programme. On n'oubliera pas d'appeler handle_unix_error pour reporter les messages d'erreurs éventuels qui peuvent se produire pendant le parcours de la hiérarchie.
(corrigé)
      
let mon_pwd () =
  try print_endline (mon_getcwd ())
  with Not_found ->
    prerr_endline "Root directory could not be found!";
    exit 1;;

handle_unix_error mon_pwd ();;

5  Parcours de l'arborescence

On désire écrire une commande mon_find qui permet d'effectuer diverses recherches dans le système de fichiers. On se limite ici à un sous-ensemble des possibilités de la commande find du système.

1) Donner des exemples de requêtes avec la commande find d'Unix (man find) permettant d'exécuter les opérations suivantes: 2) Écrire une première commande mon_find0 équivalente à find . -maxdepth 1.
(corrigé)
      
open Unix;;
open Filename;;

let mon_find0 () =
  print_endline current_dir_name(* pour faire comme find *)
  let dir_handle = opendir current_dir_name in
  let rec print_dir () =
    let name = readdir dir_handle in
    if name <> current_dir_name && name <> parent_dir_name then
      print_endline (concat current_dir_name name);
    print_dir () in
  try print_dir () with
    End_of_file -> closedir dir_handleexit 0;
  | e -> closedir dir_handleraise e;;

handle_unix_error mon_find0 ();;


3) Écrire une seconde version mon_find1 qui prend en argument une profondeur p et affiche la liste des chemins accessibles depuis le répertoire courant avec une profondeur inférieure à p.
(corrigé)
      
open Unix;;
open Filename;;

let usage_string  =
  ("Usage: " ^ Sys.argv.(0) ^ " profondeur");;

let prerr_usage () =
  prerr_endline usage_string;;


let rec mon_find depth file =
  let dir_handle = opendir file in
  let rec rec_print_dir () =
    let name = readdir dir_handle in
    if name <> current_dir_name && name <> parent_dir_name then
      begin
        let full_name = (concat file namein
        print_endline full_name;
        if depth <> 1 then
          begin
            let stat = lstat full_name in
            if stat.st_kind = S_DIR then
              mon_find (depth - 1) full_name
          end;
      end;
    rec_print_dir () in
  try rec_print_dir () with
    End_of_file -> closedir dir_handle
  | e -> closedir dir_handleraise e;;

let mon_find1 () =
  if Array.length Sys.argv <> 2 then
    begin
      prerr_usage ();
      exit 1
    end
  else
    try
      let max_depth = int_of_string Sys.argv.(1) in
      print_endline current_dir_name;
      mon_find max_depth current_dir_name;
      exit 0
    with _ -> prerr_usage (); exit 1;;

handle_unix_error mon_find1 ();;


4) Implanter les comportements équivalents aux options : Pour réaliser cette commande, on pourra utiliser le module Arg pour récupérer les arguments de la ligne de commande et le module Str (dont la librairie str.cma est à charger explicitement pour compiler) pour manipuler les expressions régulières.
(corrigé)
      
open Unix;;

(* analyse des arguments *)

let options = ref false;;
let file_list = ref [];;
let max_depth = ref max_int;;
let filter_list = ref [];;
let stat_function = ref lstat;;

let usage_string  =
  ("Usage: " ^ Sys.argv.(0) ^ " [files...] [options...]");;

let prerr_usage () =
  prerr_string usage_string;
  prerr_newline ();;

let maxdepth_opt n =
  options := true;
  max_depth := n;;

let type_opt t =
  let f = fun t n s -> s.st_kind = t in
  match t with
    "f" -> filter_list := (f S_REG) :: !filter_list
  | "l" -> filter_list := (f S_LNK) :: !filter_list
  | "d" -> filter_list := (f S_DIR) :: !filter_list
  | "b" -> filter_list := (f S_BLK) :: !filter_list
  | "c" -> filter_list := (f S_CHR) :: !filter_list
  | "s" -> filter_list := (f S_SOCK) :: !filter_list
  | "p" -> filter_list := (f S_FIFO) :: !filter_list
  | _ -> raise (Arg.Bad t) ;;

let atime_opt d =
  let f = fun n s ->
    let access_time = int_of_float (Unix.time() -. s.st_atimein
    (d + 1) * 86400 > access_time  && access_time > d * 86400 in
  filter_list := f ::!filter_list;;

let follow_opt () =
  stat_function := stat;;

let regex_opt r =
  let regexp = Str.regexp r in
  let f n s = Str.string_match regexp n 0 in
  filter_list := f :: !filter_list;;

let file_arg name =
  if !options then prerr_usage ()
  else file_list := name :: !file_list;;

(* fonctions auxilaires *)

let filter filename filestat filters =
  List.for_all (fun f -> f filename filestatfilters;;

let iter_dir f d =
  let dir_handle = opendir d in
  try while true do f (readdir dir_handledone with
    End_of_file -> closedir dir_handle
  | x -> closedir dir_handleraise x;;

let errors = ref false;;
let allow_unix_error f x =
  try f x
  with Unix_error (e,b,c) ->
    errors := true;
    prerr_string (Sys.argv.(0)^": " ^c": " ^(error_message e));
    prerr_newline();;

(* fonction principale *)
let rec find depth filename =
  let filestat = !stat_function filename in
  if filter filename filestat !filter_list then print_endline filename;
  if depth < !max_depth && filestat.st_kind = S_DIR then
    let process_child child =
      if (child <> Filename.current_dir_name &&
          child <> Filename.parent_dir_namethen
        let child_name = Filename.concat filename child in
        let childstat = !stat_function child_name in
        find (depth+1) child_name
    in
    (* process_child et iter_dir peuvent générer des erreurs *)
    (* et doivent donc être protégées *)
    allow_unix_error
      (iter_dir (allow_unix_error process_child))
      filename;;

(* le programme *)
let mon_find () =
  let opt_list =
    [ ("-maxdepth"Arg.Int maxdepth_opt"max depth search");
      ("-type"Arg.String type_opt"file type");
      ("-atime"Arg.Int atime_opt"file accessed n*24 hours ago");
      ("-follow"Arg.Unit follow_opt"follow symlinks");
      ("-regex"Arg.String regex_opt"file name matches regexp") ] in
  Arg.parse opt_list file_arg usage_string;
  filter_list := List.rev !filter_list;
  begin match !file_list with
    [] -> find 0 Filename.current_dir_name
  | _ -> List.iter (find 0) (List.rev !file_list)
  end;
  if !errors then exit 1;;

handle_unix_error mon_find();;



Ce document a été traduit de LATEX par HEVEA