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) ouvre le fichier en écriture ou en lecture ("w"/"r") (voir fopen)

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

  • 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 (voir fgets)

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

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 (voir open)

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

  • 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) (voir close)

Les types ssize_t et mode_t sont présentés dans le cours (voir write).

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 pour toutes les écritures, dans le fichier comme sur la sortie standard (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 :

  • xxd -g 1 p2e5.dat (ou hexdump -C p2e5.dat) affiche les octets bruts en hexadécimal ; -g 1 sépare chaque octet (par défaut, xxd les groupe deux par deux)

  • od -A d -t d4 p2e5.dat regroupe les octets par 4 et les affiche 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") (voir fopen)

  • 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 (voir fclose)

p2e6 : Renommer un fichier (POSIX)

Écrivez un programme p2e6.c (usage : ./a.out 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 *pathname, int mode) (#include <unistd.h>, POSIX) avec mode = F_OK, retourne 0 si le fichier pathname existe

  • int rename(const char *oldpath, const char *newpath) (#include <stdio.h>, libc) renomme oldpath en newpath (et écrase newpath 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.

Pour tester :

$ touch a c
$ ./a.out a b
$ ./a.out c b
Erreur : 'b' existe déjà, abandon.

p2e7 : Lecture partielle (seek POSIX)

Écrivez un programme p2e7.c (usage : ./a.out 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 (si le fichier est plus court, affichez seulement les octets lus).

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 (voir open)

  • 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 (voir lseek)

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

  • 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) (voir write)

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

p2e8 : cat -n (libc)

Écrivez un programme p2e8.c (usage : ./a.out 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.

Exemple avec le fichier de p2e3 :

$ ./a.out 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 (voir fopen)

  • 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, fichier)) != -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 : ./a.out [-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
}

Pour tester :

$ seq 1 20 > nombres.txt   # crée un fichier avec les nombres de 1 à 20, un par ligne
$ ./a.out -n 3 nombres.txt
1
2
3
$ ./a.out -n -1 nombres.txt
Erreur : nombre de lignes invalide : -1

p2e10 : Créer une arborescence et des liens (POSIX)

Écrivez un programme p2e10.c (usage : ./a.out 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, ./a.out 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 :

  • int mkdir(const char *pathname, 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 : ./a.out 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) : (voir open)

    • 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) (voir read)

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

  • int close(int fd) (voir close)

Utilisez un buffer de 8192 octets (char buf[8192]). C’est un multiple de la taille d’E/S préférée (Blocs d'E/S : 4096 dans la sortie de stat) : peu d’appels système, chacun efficace.

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

Avertissement

Avec O_TRUNC, ./a.out 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