• 10 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

Ce cours existe en livre papier.

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 14/02/2024

Sécurisez la saisie de texte

La saisie de texte est un des aspects les plus délicats du langage C. Vous connaissez la fonction scanf  , que vous avez vue au début du cours. Vous vous dites : quoi de plus simple et de plus naturel ? Eh bien figurez-vous que non, en fait, c'est tout sauf simple.

Ceux qui vont utiliser votre programme sont des humains. Tout humain qui se respecte fait des erreurs et peut avoir des comportements inattendus. Si vous lui demandez : « Quel âge avez-vous ? », qu'est-ce qui vous garantit qu'il ne va pas vous répondre « Je m'appelle François je vais bien merci » ?

Le but de ce chapitre est de vous faire découvrir les problèmes que l'on peut rencontrer en utilisant la fonction scanf   , et de vous montrer une alternative plus sûre avec la fonction fgets  .

Comprenez les limites de la fonction scanf

scanf() est une fonction à double tranchant :

  • elle est facile à utiliser quand on débute (c'est pour ça que je vous l'ai présentée) ;

  • mais son fonctionnement est complexe, elle peut même être dangereuse dans certains cas.

Je vais vous montrer ses limites par deux exemples concrets.

Exemple 1 : une chaîne de caractères qui contient des espaces

Supposons qu'on demande une chaîne de caractères à l'utilisateur, mais qu'il insère un espace :

#include <stdio.h>
#include <stdlib.h>
 
int main(int argc, char *argv[])
{
    char nom[20] = {0};
 
    printf("Quel est votre nom ? ");
    scanf("%s", nom);
    printf("Ah ! Vous vous appelez donc %s !\n\n", nom);
 
    return 0;
}
Quel est votre nom ? Jean Dupont
Ah ! Vous vous appelez donc Jean !

Pourquoi le Dupont a disparu ?

La fonction scanf s'arrête si elle tombe sur un espace, une tabulation ou une entrée.

Vous ne pouvez donc pas récupérer la chaîne si celle-ci comporte un espace.

Exemple 2 : une chaîne de caractères trop longue

Il y a un autre problème, beaucoup plus grave : celui du dépassement de mémoire.

Dans le code que nous venons de voir, il y a la ligne suivante :

char nom[5] = {0};

J'ai alloué 5 cases pour mon tableau de char appelé nom  , il y a donc la place d'écrire 4 caractères (le dernier étant toujours réservé au caractère de fin de chaîne\0) :

5 cases sont disponible pour stocker des caractères. Le reste des cases est marqué sur le schéma comme
Allocation de mémoire

Que se passe-t-il si on écrit plus de caractères qu'il n'y a d'espace prévu pour les stocker ?

Quel est votre nom ? Patrice
Ah ! Vous vous appelez donc Patrice !

On avait alloué 5 cases pour stocker le nom, mais en fait il en fallait 8. La fonction scanf a donc continué à écrire à la suite en mémoire comme si de rien n'était ! Elle a écrit dans des zones mémoire qui n'étaient pas prévues pour cela.

Il y a un dépassement dans la mémoire : on voit que les 5 premières cases ont été utilisées pour stocker les 5 premières lettres du prénom
Dépassement dans la mémoire

Les caractères en trop ont écrasé d'autres informations en mémoire :

Le buffer overflow c'est ce qui dépasse de la mémoire. On voit les cases qui ont pris des caractères mais n'auraient pas dû.
Le buffer overflow

Pourquoi c'est dangereux ?

Dans ce chapitre, nous allons voir comment sécuriser la saisie de nos données, en empêchant l'utilisateur de faire déborder et de provoquer un "buffer overflow".

Récupérez une chaîne de caractères

Plusieurs fonctions standard en C permettent de récupérer une chaîne de texte  :

  • gets  lit toute une chaîne de caractères mais elle ne contrôle pas les "buffer overflow" !

  • fgets  contrôle le nombre de caractères écrits en mémoire.

Nous allons donc voir comment utiliser fgets en remplacement de scanf!

Comprenez le principe de la fonction fgets

Le prototype de la fonction fgets, situé dans stdio.h, est le suivant :

char *fgets( char *str, int num, FILE *stream );

Les paramètres sont les suivants :

  • str: un pointeur vers un tableau alloué en mémoire où la fonction va pouvoir écrire le texte entré par l'utilisateur.

  • num: la taille du tableau str envoyé en premier paramètre. Si vous avez alloué un tableau de 10 charfgets lira 9 caractères au maximum (il réserve toujours un caractère d'espace pour pouvoir écrire le \0 de fin de chaîne).

  • stream: un pointeur sur le fichier à lire. Dans notre cas, le fichier à lire est l'entrée standard, c'est-à-dire le clavier. Pour demander à lire l'entrée standard, on enverra le pointeur stdin, qui est automatiquement défini dans les "headers" de la bibliothèque standard du C pour représenter le clavier. Toutefois, il est aussi possible d'utiliser fgets pour lire des fichiers, comme on a pu le voir dans le chapitre sur les fichiers.

Il suffit donc de tester si la fonction a renvoyé NULL pour savoir s'il y a eu une erreur :

#include <stdio.h>
#include <stdlib.h>
 
int main(int argc, char *argv[])
{
    char nom[10];
 
    printf("Quel est votre nom ? ");
    fgets(nom, 10, stdin);
    printf("Ah ! Vous vous appelez donc %s !\n\n", nom);
 
    return 0;
}
Quel est votre nom ? Mateo
Ah ! Vous vous appelez donc Mateo
!

Créez votre propre fonction de saisie utilisant fgets

On peut facilement créer notre propre fonction de saisie pour qu'elle fasse quelques corrections pour nous. Nous l'appellerons : lire. Elle renverra 1 si tout s'est bien passé, 0 s'il y a eu une erreur.

Éliminez le saut de ligne\n

La fonction lire va appeler fgets et , si tout s'est bien passé, va chercher le caractère\n à l'aide de la fonction strchr que l'on a déjà vue. Si un \n est trouvé, elle le remplace par un\0(fin de chaîne) pour éviter de conserver une "Entrée".

Voici le code, commenté pas à pas :

#include <stdio.h>
#include <stdlib.h>
#include <string.h> // Penser à inclure string.h pour strchr()
 
int lire(char *chaine, int longueur)
{
    char *positionEntree = NULL;
 
    // On lit le texte saisi au clavier
    if (fgets(chaine, longueur, stdin) != NULL)  // Pas d'erreur de saisie ?
    {
        positionEntree = strchr(chaine, '\n'); // On recherche l'"Entrée"
        if (positionEntree != NULL) // Si on a trouvé le retour à la ligne
        {
            *positionEntree = '\0'; // On remplace ce caractère par \0
        }
        return 1; // On renvoie 1 si la fonction s'est déroulée sans erreur
    }
    else
    {
        return 0; // On renvoie 0 s'il y a eu une erreur
    }
}

À partir du premier if, je sais si fgets s'est bien déroulée ou s'il y a eu un problème (le premier paramètre de la fonction (pointeur) est égal à (NULL), le second paramètre est inférieur ou égal à zéro, ou les deux).

Si tout s'est bien passé, je peux alors partir à la recherche du \n avec strchr et remplacer cet\n par un \0 :

La chaîne écrite par fgets était
Remplacement du saut de ligne

La chaîne écrite par fgets était "Mateo\n\0". Nous avons remplacé le \n par un \0, ce qui a donné : "Mateo\0\0". Ce n'est pas grave d'avoir deux \0 d'affilée : l'ordinateur s'arrête au premier \0 qu'il rencontre et considère que la chaîne de caractères s'arrête là :

int main(int argc, char *argv[])
{
    char nom[10];
 
    printf("Quel est votre nom ? ");
    lire(nom, 10);
    printf("Ah ! Vous vous appelez donc %s !\n\n", nom);
 
    return 0;
}
Quel est votre nom ? Mateo
Ah ! Vous vous appelez donc Mateo !
Videz le buffer

Nous ne sommes pas encore au bout de nos ennuis. Nous n'avons pas étudié ce qui se passait si l'utilisateur tentait de mettre plus de caractères qu'il n'y avait de place !

Quel est votre nom ? Jean Edouard Albert 1er
Ah ! Vous vous appelez donc Jean Edou !

Le problème, c'est que le reste de la chaîne qui n'a pas pu être lu, à savoir "ard Albert 1er", n'a pas disparu : Il est toujours dans le "buffer".

Je crois qu'un petit schéma ne sera pas de refus pour mettre les idées au clair :

Lorsque l'utilisateur tape du texte au clavier, le système d'exploitation copie directement le texte tapé dans le
Lecture du buffer du clavier

Lorsque l'utilisateur tape du texte au clavier, le système d'exploitation copie directement le texte tapé dans le "buffer" stdin. Il est là pour recevoir temporairement l'entrée du clavier.

Le rôle de la fonction fgets est justement d'extraire du "buffer" les caractères qui s'y trouvent et de les copier dans la zone mémoire que vous lui indiquez (votre tableau chaine).

Après avoir effectué son travail de copie, fgets enlève du "buffer" tout ce qu'elle a pu copier.

Si tout s'est bien passéfgets a donc pu copier tout le buffer dans votre chaîne, et ainsi le buffer se retrouve vide à la fin de l'exécution de la fonction. Mais si l'utilisateur entre beaucoup de caractères, et que la fonction fgets ne peut copier qu'une partie d'entre eux, seuls les caractères lus seront supprimés du "buffer". Tous ceux qui n'auront pas été lus y resteront !

Testons avec une longue chaîne :

int main(int argc, char *argv[])
{
    char nom[10];
 
    printf("Quel est votre nom ? ");
    lire(nom, 10);
    printf("Ah ! Vous vous appelez donc %s !\n\n", nom);
 
    return 0;
}
Quel est votre nom ? Jean Edouard Albert 1er
Ah ! Vous vous appelez donc Jean Edou !

La fonction fgets a copié les 9 premiers caractères. Les autres sont toujours dans le buffer :

Ce que la fonction fgets n'a pas pu lire, elle le laisse dans le buffer.
Lecture du buffer du clavier avec débordement

Testons ce code :

int main(int argc, char *argv[])
{
    char nom[10];
 
    printf("Quel est votre nom ? ");
    lire(nom, 10);
    printf("Ah ! Vous vous appelez donc %s !\n\n", nom);
    lire(nom, 10);
    printf("Ah ! Vous vous appelez donc %s !\n\n", nom);
 
    return 0;
}

Nous appelons deux fois la fonction lire. Pourtant, on ne vous laisse pas taper deux fois votre nom : la fonction fgets ne demande pas à l'utilisateur de taper du texte la seconde fois car elle trouve du texte à récupérer dans le "buffer" !

Quel est votre nom ? Jean Edouard Albert 1er
Ah ! Vous vous appelez donc Jean Edou !
 
Ah ! Vous vous appelez donc ard Alber !

On va donc améliorer notre fonctionlireet appeler une sous-fonction viderBuffer pour faire en sorte que le "buffer" soit vidé si on a rentré trop de caractères :

void viderBuffer()
{
    int c = 0;
    while (c != '\n' && c != EOF)
    {
        c = getchar();
    }
}
 
int lire(char *chaine, int longueur)
{
    char *positionEntree = NULL;
 
    if (fgets(chaine, longueur, stdin) != NULL)
    {
        positionEntree = strchr(chaine, '\n');
        if (positionEntree != NULL)
        {
            *positionEntree = '\0';
        }
        else
        {
            viderBuffer();
        }
        return 1;
    }
    else
    {
        viderBuffer();
        return 0;
    }
}

La fonction lire appelle viderBuffer dans deux cas :

  1. Si la chaîne était trop longue (on le sait parce qu'on n'a pas trouvé de caractère \n dans la chaîne copiée).

  2. S'il y a eu une erreur (peu importe laquelle), il faut vider là aussi le "buffer" par sécurité pour qu'il n'y ait plus rien.

Convertissez la chaîne en nombre

Notre fonction lire est maintenant efficace mais elle ne sait lire que du texte.

Mais comment fait-on pour récupérer un nombre ?

Avec fgets, vous ne pouvez récupérer que du texte, mais il existe d'autres fonctions qui permettent de convertir ensuite un texte en nombre.

Convertissez une chaîne en long avec strtol

Le prototype de la fonction strtol est un peu particulier :

long strtol( const char *start, char **end, int base );
  1. La fonction lit la chaîne de caractères que vous lui envoyez :start.

  2. Elle essaie de la convertir en long en utilisant la base indiquée (généralement, on travaille en base 10 car on utilise 10 chiffres différents de 0 à 9, donc vous mettrez 10).

  3. Elle retourne le nombre qu'elle a réussi à lire.

Quant au pointeur de pointeur end, la fonction s'en sert pour renvoyer la position du premier caractère qu'elle a lu et qui n'était pas un nombre. On ne s'en servira pas, on peut donc lui envoyer NULL pour lui faire comprendre qu'on ne veut rien récupérer.

Voici quelques exemples d'utilisation pour bien comprendre le principe :

long i;
 
i = strtol( "148", NULL, 10 ); // i = 148
i = strtol( "148.215", NULL, 10 ); // i = 148
i = strtol( "   148.215", NULL, 10 ); // i = 148
i = strtol( "   148+34", NULL, 10 ); // i = 148
i = strtol( "   148 feuilles mortes", NULL, 10 ); // i = 148
i = strtol( "   Il y a 148 feuilles mortes", NULL, 10 ); // i = 0 (erreur : la chaîne ne commence pas par un nombre)

Toutes les chaînes qui commencent par un nombre (ou éventuellement des espaces suivis d'un nombre) seront converties en long jusqu'à la première lettre ou jusqu'au premier caractère invalide :.+, etc.

La dernière chaîne de la liste ne commençant pas par un nombre, elle ne peut pas être convertie.

La fonction strtol renverra donc 0.

On peut créer une fonction lireLong qui va appeler notre première fonction lire(qui lit du texte), et ensuite convertir le texte saisi en nombre :

long lireLong()
{
    char nombreTexte[100] = {0}; // 100 cases devraient suffire
 
    if (lire(nombreTexte, 100))
    {
        // Si lecture du texte ok, convertir le nombre en long et le retourner
        return strtol(nombreTexte, NULL, 10);
    }
    else
    {
        // Si problème de lecture, renvoyer 0
        return 0;
    }
}

Vous pouvez tester dans un main très simple :

int main(int argc, char *argv[])
{
    long age = 0;
 
    printf("Quel est votre age ? ");
    age = lireLong();
    printf("Ah ! Vous avez donc %d ans !\n\n", age);
 
    return 0;
}
Quel est votre age ? 18
Ah ! Vous avez donc 18 ans !

Convertissez une chaîne en double avec strtod

La fonction strtod est identique à strtol   , sauf qu'elle essaie de lire un nombre décimal et renvoie un double:

double strtod( const char *start, char **end );

Le troisième paramètre base a disparu ici, mais il y a toujours le pointeur de pointeur end qui ne nous sert à rien.

Essayez d'écrire la fonction lireDouble (c'est exactement comme lireLong  , à part que cette fois, on appelle strtod et on retourne undouble). Vous devriez alors pouvoir faire ceci :

Combien pesez-vous ? 67.4
Ah ! Vous pesez donc 67.400000 kg !

Ensuite, modifiez votre fonction lireDouble pour qu'elle accepte aussi le symbole virgule comme séparateur décimal :

  1. Remplacez la virgule par un point dans la chaîne de texte lue (grâce à la fonction de recherche strchr).

  2. Puis envoyez la chaîne modifiée à strtod.

En résumé

  • La fonction scanf a des limites. On ne peut pas, par exemple, écrire plusieurs mots à la fois facilement.

  • Un "buffer overflow" survient lorsqu'on dépasse l'espace réservé en mémoire : si l'utilisateur entre 10 caractères alors qu'on n'avait réservé que 5 cases en mémoire.

  • L'idéal est de faire appel à la fonction fgets pour récupérer du texte saisi par l'utilisateur.

  • Il faut en revanche éviter à tout prix d'utiliser la fonction gets qui n'offre pas de protection contre le "buffer overflow".

  • Vous pouvez écrire votre propre fonction de saisie du texte qui fait appel à fgets comme on l'a fait, afin d'améliorer son fonctionnement.

J’ai l’impression que vous avez terminé la partie 2 de ce cours. Ne changeons pas les bonnes habitudes, il est temps de faire un quiz afin d’évaluer vos nouvelles connaissances !

Exemple de certificat de réussite
Exemple de certificat de réussite