Partie 2 - Fichiers

Pour les exercices ne demandant pas d’écrire du code C, créez un fichier Markdown (p2e[numéro exo].md) pour lister les commandes utilisées, leurs sorties et votre interprétation.

Pour les exercices en C, mettez #define _POSIX_C_SOURCE 200809L en première ligne du fichier (avant les #include) : c’est une bonne pratique, qui garantit que les fonctions et constantes POSIX sont déclarées même avec -std=c2x (voir la note du cours).

Note

Si vous travaillez dans le dossier partagé avec Windows, certaines opérations ne fonctionnent pas comme sous Linux : la création de liens échoue et le nombre de blocs affiché par stat peut différer. Faites alors les exercices dans votre répertoire personnel (cd ~).

p2e1 - Métadonnées et types (shell)

Créez 3 fichiers :

  • un fichier texte : echo "hello" > p2e1.txt,

  • un fichier binaire : head -c 5000 /dev/urandom > p2e1.dat,

  • un script shell p2e1.sh, créé avec votre éditeur, contenant :

    #!/usr/bin/env bash
    echo "coucou"
    

    La première ligne (le shebang) indique quel interpréteur exécute le script (ici bash). Rendez ensuite le script exécutable avec chmod +x p2e1.sh.

Affichez leurs métadonnées avec stat et leur nature avec file.

Comparez les champs clés (type, taille, blocs, droits) et répondez aux questions :

  • Quel est le type de contenu de chaque fichier d’après file ?

  • Quelle différence de droits y a-t-il entre le script et les deux autres fichiers ?

Si les droits observés diffèrent de ceux du cours (par exemple rw-rw-r-- au lieu de rw-r--r--), c’est dû au masque de création de votre machine (voir Droits à la création : notation octale et umask dans le cours).

Commandes :

  • stat FICHIER : taille logique, blocs, mode (permissions), inode,

  • file FICHIER : type de contenu (texte ASCII, binaire, script, etc.).

p2e2 - Liens physiques et symboliques (shell)

Créez un fichier p2e2_source.txt contenant une ligne de texte (echo "coucou" > p2e2_source.txt).

Créez un lien symbolique p2e2_sym.txt et un lien physique p2e2_hard.txt vers p2e2_source.txt.

Observez la sortie de ls -li et de stat pour les 3 fichiers. Modifiez le contenu de p2e2_source.txt depuis le shell (echo "modif" >> p2e2_source.txt), puis affichez le contenu des deux liens.

Supprimez le fichier p2e2_source.txt, puis observez à nouveau ls -li, stat et cat sur p2e2_hard.txt et p2e2_sym.txt.

Répondez aux questions :

  • Quels fichiers ont le même numéro d’inode ?

  • Combien de liens (Liens :) a chaque fichier ?

  • Quelle est la taille du lien symbolique et à quoi correspond-elle ?

  • Que devient chaque lien après la suppression de p2e2_source.txt ?

Commandes :

  • ln CIBLE NOM_DU_LIEN (lien physique), ln -s CIBLE NOM_DU_LIEN (lien symbolique),

  • ls -li : l’option -i affiche le numéro d’inode de chaque fichier.

p2e3 - E/S texte (libc)

Écrivez un programme p2e3.c qui utilise les fonctions de la libc pour manipuler les fichiers (fopen, fclose…). Il doit ouvrir un fichier p2e3_anneau.txt en écriture et écrire :

Un Anneau pour les gouverner tous,
un Anneau pour les trouver,
un Anneau pour les amener tous et dans les ténèbres les lier.

Puis le programme ferme le fichier, le rouvre en lecture et affiche son contenu ligne par ligne sur la sortie standard : chaque ligne est lue avec fgets et affichée avec fputs(ligne, stdout) (ou printf).

Fonctions :

  • FILE *fopen(const char *pathname, const char *mode) pour ouvrir le fichier en écriture ou en lecture ("w"/"r"),

  • int fprintf(FILE *stream, const char *format, ...) pour écrire une chaîne formatée format dans le flux (fichier/stdout/stderr),

  • int fputs(const char *s, FILE *stream) écrit s dans le flux (fichier/stdout/stderr),

  • char *fgets(char *s, int size, FILE *stream) lit au plus size - 1 caractères depuis le flux stream et les place dans s ; la lecture s’arrête après un \n (qui est conservé), puis '\0' est ajouté ; retourne NULL en fin de fichier ou en cas d’erreur,

  • int fclose(FILE *stream) ferme le flux.

p2e4 - E/S texte (POSIX)

Écrivez un programme p2e4.c qui utilise les fonctions POSIX pour manipuler les fichiers (open, close…). Il doit ouvrir un fichier p2e4_nazg.txt en écriture et écrire :

Ash nazg durbatulûk,
ash nazg gimbatul,
ash nazg thrakatulûk agh burzum-ishi krimpatul

Puis le programme ferme le fichier, le rouvre en lecture, lit son contenu avec read et l’affiche sur la sortie standard avec write(STDOUT_FILENO, …) (ou write(1, …)).

Pour les includes :

#define _POSIX_C_SOURCE 200809L // utilise la version de 2008 de POSIX
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

Fonctions :

  • int open(const char *pathname, int flags) ou int open(const char *pathname, int flags, mode_t mode) pour ouvrir le fichier : à la création, utilisez les flags O_WRONLY | O_CREAT | O_TRUNC et le mode 0644 (le mode est obligatoire avec O_CREAT) ; pour la lecture, utilisez le flag O_RDONLY, sans mode,

  • ssize_t write(int fd, const void *buf, size_t count) pour écrire dans fd (file descriptor) count octets de buf ; retourne le nombre d’octets écrits (qui peut être inférieur à count) ou -1,

  • ssize_t read(int fd, void *buf, size_t count) lit au plus count octets ; lectures partielles possibles → boucler jusqu’à 0 (fin de fichier) ; retourne -1 en cas d’erreur (voir read),

  • int close(int fd).

ssize_t est un size_t signé (il peut valoir -1), mode_t représente les droits du fichier (voir le cours).

write peut écrire moins d’octets que demandé, ou être interrompu par un signal (errno == EINTR, les signaux seront vus plus tard) : la fonction write_full du cours répète les appels à write jusqu’à ce que tout soit écrit. Copiez-la dans votre programme et utilisez-la à la place de write (elle sera réutilisée dans les parties suivantes).

Avertissement

read n’ajoute pas de '\0' à la fin des octets lus : n’affichez pas le buffer avec printf("%s", buf), utilisez le nombre d’octets renvoyé par read.

Question : si le programme ouvrait le fichier une seule fois avec O_RDWR, écrivait le texte, puis appelait read sans fermer ni rouvrir le fichier, que lirait read ? Pourquoi (voir Position courante : lseek dans le cours) ?

p2e5 - E/S binaire (libc)

Écrivez un programme p2e5.c qui initialise un tableau d”int32_t avec {INT32_MIN, INT32_MIN + 1, -2048, -1, 0, 1, 2048, INT32_MAX - 1, INT32_MAX} (int32_t, INT32_MIN/MAX disponibles avec #include <stdint.h>), puis l’écrit dans un fichier binaire p2e5.dat via fwrite.

Puis rouvrez le fichier, lisez les valeurs avec fread, affichez-les et vérifiez qu’elles sont identiques au tableau initial.

Vérifiez la taille du fichier avec la commande stat et comparez-la à sizeof(int32_t) * 9.

Inspectez le contenu du fichier avec xxd -g 1 p2e5.dat (ou hexdump -C p2e5.dat) (octets bruts en hexadécimal, -g 1 sépare chaque octet ; par défaut xxd les groupe deux par deux), et avec od -A d -t d4 p2e5.dat (octets regroupés par 4 et affichés comme entiers signés : on doit retrouver les valeurs du tableau ; -A d affiche les décalages en décimal, sinon od les affiche en octal).

xxd affiche les octets dans l’ordre du fichier (ici l’ordre mémoire, puisque fwrite copie les octets tels quels). En little-endian, un mot 32 bits 0xAABBCCDD est stocké DD CC BB AA. Pour reconstituer la valeur, il faut réassembler les 4 octets dans l’ordre inverse (ou utiliser od/hexdump formatés).

Fonctions :

  • FILE *fopen(const char *pathname, const char *mode) pour ouvrir le fichier en écriture ou en lecture binaire ("wb"/"rb"),

  • size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream) écrit dans stream nmemb éléments de size octets chacun, lus à l’adresse ptr (ici size = sizeof(int32_t) et nmemb = nombre d’éléments du tableau) ; retourne le nombre d’éléments écrits,

  • size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream) lit au plus nmemb éléments de size octets depuis le flux stream et les place dans ptr ; retourne le nombre d’éléments lus,

  • int fclose(FILE *stream) ferme le flux.

p2e6 - Renommer un fichier (POSIX)

Écrivez un programme p2e6.c (usage : ./p2e6 OLD NEW) qui renomme le fichier OLD en NEW si NEW n’existe pas ; sinon, il affiche un message d’erreur.

Fonctions :

  • int access(const char *path, int amode) (#include <unistd.h>, POSIX) avec amode = F_OK, retourne 0 si le fichier path existe,

  • int rename(const char *old, const char *new) (#include <stdio.h>, libc) renomme old en new (et écrase new s’il existe).

Note

Cette solution n’est pas parfaite : NEW peut être créé par un autre programme entre l’appel à access et l’appel à rename (on parle de race condition). De plus, access suit les liens symboliques : si NEW est un lien symbolique cassé, access indique qu’il n’existe pas et rename l’écrase.

p2e7 - Lecture partielle (seek POSIX)

Écrivez un programme p2e7.c (usage : ./p2e7 FICHIER) qui lit, avec les fonctions POSIX, les 10 octets situés après les 15 premiers du fichier FICHIER, puis les affiche sur la sortie standard avec write, suivis d’un \n.

Avec un fichier contenant :

ABCDEFGHIJKLMNOPQRSTUVWXYZ

(créé avec echo ABCDEFGHIJKLMNOPQRSTUVWXYZ > p2e7.txt)

Le programme affiche :

PQRSTUVWXY

Fonctions :

  • int open(const char *pathname, int flags) avec le flag O_RDONLY pour ouvrir le fichier en lecture,

  • off_t lseek(int fd, off_t offset, int whence) pour sauter les 15 premiers octets (offset = 15), avec whence = SEEK_SET (décalage compté depuis le début du fichier) ; off_t est un entier représentant une position dans un fichier,

  • ssize_t read(int fd, void *buf, size_t count) lit au plus count octets depuis fd et les place dans buf,

  • ssize_t write(int fd, const void *buf, size_t count) avec fd = STDOUT_FILENO (ou 1) pour écrire sur la sortie standard (voir p2e4),

  • int close(int fd) pour fermer le fichier.

p2e8 - cat -n (libc)

Écrivez un programme p2e8.c (usage : ./p2e8 FICHIER) qui affiche le contenu du fichier FICHIER en numérotant les lignes (à la manière de cat -n, avec le format ci-dessous). Chaque ligne est précédée de son numéro, affiché sur au moins 4 caractères (%4d), suivi de : et d’un espace. Le fichier est lu ligne par ligne avec getline (qui nécessite #define _POSIX_C_SOURCE 200809L en première ligne).

Exemple avec le fichier de p2e3 :

$ ./p2e8 p2e3_anneau.txt
   1: Un Anneau pour les gouverner tous,
   2: un Anneau pour les trouver,
   3: un Anneau pour les amener tous et dans les ténèbres les lier.

Fonctions de la libc :

  • FILE *fopen(..., "r") et fclose,

  • ssize_t getline(char **lineptr, size_t *n, FILE *stream) lit la prochaine ligne du flux stream (avec le \n) et la place dans *lineptr ; retourne le nombre de caractères lus, ou -1 en fin de fichier ou en cas d’erreur. Si *lineptr vaut NULL (et *n vaut 0), getline alloue un buffer et met à jour *lineptr et *n ; le buffer est agrandi si nécessaire. Il faut le libérer avec free, même quand getline renvoie -1,

  • int feof(FILE *stream) et int ferror(FILE *stream) : après un -1, permettent de savoir si on est à la fin du fichier (feof non nul) ou s’il y a eu une erreur (ferror non nul).

Appel type de getline :

char *line = NULL; // getline alloue le buffer
size_t cap = 0;    // taille du buffer alloué
ssize_t len;
while ((len = getline(&line, &cap, f)) != -1) {
    // line contient la ligne lue (avec le \n), len est sa longueur
}
free(line);

p2e9 - head -n (libc)

Écrivez un programme p2e9.c (usage : ./p2e9 [-n N] FICHIER) qui affiche les N premières lignes du fichier FICHIER (défaut 10 lignes). N doit être un entier positif ou nul ; sinon, affichez un message d’erreur. Le fichier est lu ligne par ligne comme dans p2e8 (fopen, getline, fclose).

Fonctions :

  • int strcmp(const char *s1, const char *s2) retourne 0 si s1 et s2 sont égaux,

  • INT_MAX (#include <limits.h>) : plus grande valeur d’un int, pour vérifier que N tient dans un int,

  • long strtol(const char *nptr, char **endptr, int base) retourne un long correspondant au nombre écrit dans nptr avec la base donnée ; *endptr pointe ensuite sur le premier caractère non converti, ce qui permet de détecter une chaîne qui n’est pas un nombre ; si le nombre est trop grand pour un long, strtol renvoie LONG_MAX (ou LONG_MIN) et met errno à ERANGE (#include <errno.h>) :

char *end;
errno = 0;
long v = strtol(argv[i], &end, 10);
if (end == argv[i] || *end != '\0' || errno == ERANGE) {
    // argv[i] n'est pas un nombre entier, ou il est trop grand
}

p2e10 - Créer une arborescence + liens (POSIX)

Écrivez un programme p2e10.c (usage : ./p2e10 rep_name nb_d nb_f, on suppose nb_d >= 1 et nb_f >= 2) qui crée :

  • un répertoire rep_name,

  • pour chaque d ∈ [1..nb_d] : un répertoire rep_name/sous_rep_d/,

  • pour chaque d et chaque f ∈ [1..nb_f] : un fichier vide rep_name/sous_rep_d/sous_rep_d_fichier_f.txt,

  • au niveau racine rep_name/ :

    • un lien physique lien_physique_vers_sous_rep_1_fichier_1.txt → sous_rep_1/sous_rep_1_fichier_1.txt,

    • un lien symbolique lien_symbolique_vers_sous_rep_1_fichier_2.txt → sous_rep_1/sous_rep_1_fichier_2.txt.

Par exemple, ./p2e10 rep 2 3 crée :

$ tree rep
rep
├── lien_physique_vers_sous_rep_1_fichier_1.txt
├── lien_symbolique_vers_sous_rep_1_fichier_2.txt -> sous_rep_1/sous_rep_1_fichier_2.txt
├── sous_rep_1
│   ├── sous_rep_1_fichier_1.txt
│   ├── sous_rep_1_fichier_2.txt
│   └── sous_rep_1_fichier_3.txt
└── sous_rep_2
    ├── sous_rep_2_fichier_1.txt
    ├── sous_rep_2_fichier_2.txt
    └── sous_rep_2_fichier_3.txt

2 directories, 8 files

Si le programme est relancé alors que les éléments existent déjà, il doit afficher un message d’erreur et se terminer avec un code d’erreur.

Fonctions (symlink nécessite #define _POSIX_C_SOURCE 200809L en première ligne) :

  • int mkdir(const char *path, mode_t mode) (#include <sys/stat.h>, ex. : mode = 0755, voir Droits à la création : notation octale et umask dans le cours pour la notation octale) ; échoue avec errno == EEXIST si le répertoire existe déjà,

  • création d’un fichier : open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644) puis close,

  • liens (#include <unistd.h>) :

    • int link(const char *oldpath, const char *newpath) (physique),

    • int symlink(const char *target, const char *linkpath) (symbolique),

  • conversion des arguments en nombres : strtol (voir p2e9),

  • construction de chemins : int snprintf(char *str, size_t size, const char *format, ...) (#include <stdio.h>) fonctionne comme printf mais écrit au plus size caractères ('\0' compris) dans str ; retourne le nombre de caractères qu’il aurait fallu écrire (une valeur >= size indique que le résultat a été tronqué) :

char path[256];
int d = 1;
int ret = snprintf(path, sizeof path, "%s/sous_rep_%d", "rep", d);
if (ret < 0 || (size_t)ret >= sizeof path) {
    // erreur ou chemin tronqué
}
// path contient "rep/sous_rep_1"

Note

Pour link, oldpath est relatif au répertoire courant (il faut donc écrire rep_name/sous_rep_1/…). Pour symlink, target est stocké tel quel dans le lien et sera interprété relativement au répertoire qui contient le lien : il faut donc écrire sous_rep_1/sous_rep_1_fichier_2.txt (et non rep_name/sous_rep_1/…, qui donnerait un lien cassé).

p2e11 - Copier un fichier (POSIX)

Écrivez un programme p2e11.c (usage : ./p2e11 SOURCE DEST) qui copie SOURCE vers DEST en utilisant les appels système POSIX de bas niveau.

Fonctions :

  • int open(const char *pathname, int flags) ou int open(const char *pathname, int flags, mode_t mode) :

    • source : open(pathname, O_RDONLY),

    • destination : open(pathname, O_WRONLY | O_CREAT | O_TRUNC, 0644),

  • ssize_t read(int fd, void *buf, size_t count),

  • ssize_t write(int fd, const void *buf, size_t count),

  • int close(int fd).

Utilisez un buffer (char buf[]) de 8192 octets : un buffer de cette taille limite le nombre d’appels système, et choisir un multiple de la taille d’E/S préférée (Blocs d'E/S : 4096 dans la sortie de stat) rend chaque appel efficace.

Pour les écritures, réutilisez la fonction write_full du cours (comme dans p2e4).

Avertissement

Avec O_TRUNC, ./p2e11 f f vide le fichier f avant de le lire : le contenu est perdu. Ne testez pas avec la même source et la même destination.

Testez avec un fichier de plus de 8192 octets et vérifiez que tout est copié :

# pour télécharger Dracula de Bram Stoker (domaine public - Project Gutenberg)
wget https://www.gutenberg.org/cache/epub/345/pg345.txt

Avec strace -e trace=read,write (qui n’affiche que les appels read et write), observez les appels read(3, …, 8192) = 8192 et write(4, …, 8192) = 8192 qui se répètent, puis le dernier read qui renvoie 0 (fin de fichier).

$ # -Wconversion signale les conversions implicites qui peuvent perdre de l'information
$ gcc -std=c2x -Wall -Wextra -pedantic -Wconversion -g p2/p2e11.c
$ strace -e trace=read,write ./a.out pg345.txt dracula.txt
...
...
$ # si les valeurs de sha256sum sont les mêmes, le contenu est le même
$ sha256sum dracula.txt pg345.txt
<empreinte>  dracula.txt
<empreinte>  pg345.txt
$ # si cmp n'affiche rien, les fichiers sont les mêmes
$ cmp dracula.txt pg345.txt