Aller au menu - Aller au contenu

Icône Réaliser des saisies sécurisées grâce à fgets

Mise à jour : 22/08/2008
311 visites depuis 7 jours, classé 333/786
Vous avez fait un programme que vous jugez utile (pour quelqu'un d'autre que vous), mais vous utilisez encore scanf(), et vous savez qu'il plante souvent à cause de cela ? J'ai la solution. Nous allons d'abord étudier le comportement de cette fonction puis nous verrons la solution à ces problèmes.

Étude de la fonction scanf()

Dans la plupart des cours de C, scanf() est une des premières fonctions présentée aux débutants. Malgré son apparence d'utilisation "facile", elle est dur à maîtriser et demande une surcharge de code (pour résoudre tous les problèmes qu'elle cause par son utilisation).

Nous allons donc, étudier cette fonction, et voir quels problèmes se posent lors de son utilisation.

Les attributs de conversion



scanf() demande une chaîne à un utilisateur et s'occupe de le convertir grâce aux attributs de conversion qui sont spécifiés par le programmeur.

Généralement, on utilise ces différents attributs :
  • %ld pour une conversion vers une variable de type long.
  • %lf pour un type double.
  • %s pour saisir une chaîne.


Il existe beaucoup de ces attributs, je fournis ceux qui sont le plus utilisés, vous pouvez trouver une liste plus complète en tapant man scanf dans votre moteur de recherche.

Les problèmes



Oui, il existe des problèmes en relation avec une mauvaise utilisation de scanf(), nous allons traiter des deux problèmes les plus courants dans l'utilisation de scanf().

Entrer des caractères inattendus dans un appel à scanf()


Nous allons prendre un exemple très simple, vous voulez que l'utilisateur entre un nombre et vous voulez le récupérer dans une variable.

Quoi de plus simple avec notre ami scanf() !
Code : C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <stdio.h>

int main (void)
{
    int nombre;
    
    printf("Entre un nombre s'il-te-plait : \n");
    scanf("%d", &nombre);
    printf("Merci ! le nombre que vous avez tape est %d", nombre);
    
    return 0;
}

Ce qui donne après compilation et exécution :
Code : Console
Entre un nombre s'il-te-plait :

50

Merci ! Le nombre que vous avez tape est 50

Jusque-là, rien d'anormal.

Maintenant, relançons notre programme mais cette fois-ci en entrant quelques caractères :
Code : Console
Entre un nombre s'il-te-plait : dgsgcbv

[...]


On entre dans une boucle infinie, ce problème est très embêtant.

Mais il ne faut pas en vouloir à scanf, elle fait son job. Le problème, c'est qu'elle a été conçue pour avoir des saisies dîtes "formatées" (d'où le nom : scanformatted) c'est-à-dire, une saisie qui respecte caractère pour caractère ce que vous avez mis dans son appel (donc pour une saisie de chiffre, elle ne cherche pas à savoir si c'est un nombre ou pas, elle le stocke quand même dans votre int).

Entrer une chaîne de caractères



Vous souhaitez que l'utilisateur entre une chaîne dans votre programme ? Ok, on utilise scanf.
Code : C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <stdio.h>

int main (void)
{
    char chaine[20];

    printf("Ecris une phrase s'il-te-plait : \n");
    scanf("%s", chaine);
    printf("Tu as entre : '%s'", chaine);

    return 0;
}


On teste notre programme :
Code : Console
Ecris une phrase s'il-te-plait :

Salut ça va ?

Tu as entre : 'Salut'


o_O Oui, scanf n'a pris que le premier mot de notre phrase !

Pourquoi ? C'est très simple, avec l'utilisation du format %s, scanf supprime tous les espaces qu'il juge inutiles (avant ou après le premier mot de la chaîne), le comportement est facile à deviner, scanf ne prend que le premier mot (avant le premier espace) et le stocke dans la chaîne.

Où sont les caractères restants ? Dans le flux d'entrée standard, stdin.

Et cela pose problème, effectivement, lors du prochain appel à scanf, les caractères non extraits (qui ne sont pas dans la chaîne) se trouvent dans stdin et vont directement se stocker dans la chaîne sans que l'utilisateur n'ait rien à demander.

Pour "manger" les caractères restants, on va tout simplement les lire, car ils sont non lus.

Pour cela, on va utiliser la macro très utilisée : getchar() (ou la fonction fgetc(stdin) ).

On va coder ça dans une fonction :
Code : C
1
2
3
4
5
6
7
static void purger(void)
{
    int c;

    while ((c = getchar()) != '\n' && c != EOF)
    {}
}


On va tester ça avec un petit programme :
Code : C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>

static void purger(void)
{
    int c;

    while ((c = getchar()) != '\n' && c != EOF)
    {}
}

int main (void)
{
    char chaine[20], chaine2[20];

    printf("Ecris une phrase s'il-te-plait : \n");
    scanf("%s", chaine);
    printf("Tu as entre : '%s'", chaine);
    purger();
    printf("\nEcris une autre phrase s'il-te-plait : \n");
    scanf("%s", chaine2);
    printf("\nTu as entre : '%s'", chaine2);
    purger();

    return 0;
}

Et l'exécution :
Code : Console
Ecris une phrase s'il-te-plait :

Salut ça va ?

Tu as entre : 'Salut'

Ecris une autre phrase s'il-te-plait :

Bien et toi ?



Tu as entre : 'Bien'

On voit que ce problème est résolu !

Vous avez vu le gros des problèmes. Vous voulez résoudre ça ? C'est parti pour la seconde partie !

Captures sécurisées grâce à fgets() et une fonction de conversion !

Nous entrons dans la partie la plus intéressante : La résolution des problèmes causés par scanf().

Mais nous allons rencontrer de nouveaux problèmes, nous allons les prévoir puis les résoudre.

Comportement



Eh oui ! Encore un peu de théorie pour bien comprendre ce que l'on fait (le C est un langage de bas niveau donc il est préférable de bien connaître le fonctionnement des fonctions standards). Cette fois, ce ne sera pas long. Vous allez voir !

Le prototype de fgets est le suivant :
Code : C
1
char *fgets (char *str, int size, FILE* stream);

fgets lit size-1 caractères dans stream et place le tout dans str (avec le caractère terminal '\0').

Le pointeur sur FILE peut être un fichier mais aussi l'entrée standard (c'est le flux stdin, il contient les caractères saisis).

La fonction extrait le '\n' (retour chariot qui est la validation de la frappe) dans le flux quand elle le peut (selon l'espace libre dans la chaîne à ce moment), si ce caractère est présent à la fin de la chaîne, on sait que la saisie est réussie.

Il ne faut pas confondre les fonctions fgets et gets. Cette dernière si on l'utilise, augmente énormément le risque de buffer overflow (entrer plus de caractères dans une chaîne que sa limite).


Problèmes



Voilà, vous avez les connaissances pour utiliser fgets() correctement. On va mettre cela en pratique :
Code : C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <stdio.h>

int main (void)
{
    char chaine[20];

    printf("Tapez une phrase : \n");
    fgets(chaine, sizeof chaine, stdin);
    printf("Vous avez tape : '%s'", chaine);

    return 0;
}


Cela donne un résultat satisfaisant :
Code : Console
Tapez une phrase :

Salut ça va ?

Vous avez tape : 'Salut ça va ?

'


Premier problème : On voit qu'il y a bien un retour à la ligne à la fin de la chaîne.

On va le localiser grâce à une fonction standard : strchr() (elle est dans l'header string.h).

On code cela :
Code : C
1
2
3
4
5
6
7
8
9
static void search(char *chaine)
{
    char *p = strchr(chaine, '\n');

    if (p)
    {
        *p = 0;
    }
}


Le code est très simple : on cherche le caractère \n et on le supprime (grâce au pointeur, on le déférence).

Il faut inclure <string.h> pour la fonction strchr() !


Un petit test :
Code : C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <string.h>

static void search(char *chaine)
{
    char *p = strchr(chaine, '\n');

    if (p)
    {
        *p = 0;
    }
}

int main (void)
{
    char chaine[20];

    printf("Tapez une phrase : \n");
    fgets(chaine, sizeof chaine, stdin);
    search(chaine);
    printf("Vous avez tape : '%s'", chaine);

    return 0;
}

Code : Console
Tapez une phrase :

Salut ça va ?

Vous avez tape : 'Salut ça va ?'


On voit que notre programme marche correctement.

Cependant, nous avons un deuxième problème : Si la dernière saisie ne s'est pas déroulée correctement (ou on a dépassé le nombre de caractères max spécifié en deuxième argument) alors la seconde saisie va contenir les caractères qui n'ont pas été lus et ainsi de suite, jusqu'à ce que le flux stdin devienne vide.

Nous avons déjà traité ce problème donc on reprend notre fonction :
Code : C
1
2
3
4
5
6
7
static void purger(void)
{
    int c;

    while ((c = getchar()) != '\n' && c != EOF)
    {}
}

Un petit test :
Code : C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdio.h>
#include <string.h>

static void purger(void)
{
    int c;

    while ((c = getchar()) != '\n' && c != EOF)
    {}
}

static void clean (char *chaine)
{
    char *p = strchr(chaine, '\n');

    if (p)
    {
        *p = 0;
    }

    else
    {
        purger();
    }
}

int main (void)
{
    char chaine[20], chaine2[20];

    printf("Tapez une phrase : \n");
    fgets(chaine, sizeof chaine, stdin);
    clean(chaine);
    printf("Vous avez tape : '%s'", chaine);

    printf("Tapez une phrase : \n");
    fgets(chaine2, sizeof chaine2, stdin);
    clean(chaine2);
    printf("Vous avez tape : '%s'", chaine2);

    return 0;
}


Code : Console
Tapez une phrase :

123456789123456789123456789

Vous avez tape : '1234567891234567891'

Tapez une phrase :

123456789123456789123456789

Vous avez tape : '1234567891234567891'

On voit que tout marche correctement !

Voilà, mais néanmoins, il reste encore un problème : Toutes nos saisies avec fgets sont pour le moment des chaînes, elles sont toutes stockées dans des chaînes de caractères.

Nous allons traiter la conversion de chaîne vers un entier ou un réel.

Conversion de chaîne à entier ou réel



Vous avez plusieurs solutions :
  • sscanf (équivalent à scanf, nous verrons une utilisation sécurisé de la conversion grâce à cette fonction).
  • les fonctions strto* (l ou d).


sscanf

sscanf fonctionne exactement de la même manière que scanf à l'exception qu'il utilise une chaîne pour convertir ses éléments à la place du flux stdin.

Voici un code commenté pour bien comprendre le fonctionnement :
Code : C
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <stdio.h>
#include <string.h> /* Pour la fonction clean (strchr). */

/* Pour supprimer le '\n' de fgets et nettoyer le buffer stdin (entrée
   standard). */
static void clean(const char *buffer);
static void purger(void);

int main(void)
{
    /* La chaine qui va contenir les caracteres. */
    char chaine[20];

    /* Le resultat recherche : un nombre. */
    int nombre;

    /* Un booleen pour savoir si la chaine est valide. */
    int ret;

    /* Tant que la chaine est invalide, elle ne comporte pas ce que
       l'on veut, on continue. */
    do
    {
        /* On demande a l'utilisateur d'entrer son nombre. */
        printf("Entrez votre nombre : ");
        fgets(chaine, sizeof chaine, stdin);

        /* On nettoie notre chaine et le buffer stdin. */
        clean(chaine);

        /* On convertie notre chaine en entier. */
        ret = sscanf(chaine, "%d", &nombre);
    } while (ret != 1);

    /* On voit que notre nombre est bien dans le int. */
    printf("Nombre: %d", nombre);

    return 0;
}

static void purger(void)
{
    int c;

    while ((c = getchar()) != '\n' && c != EOF)
    {}
}

static void clean (char *chaine)
{
    char *p = strchr(chaine, '\n');

    if (p)
    {
        *p = 0;
    }

    else
    {
        purger();
    }
}


strtol

Cette fonction sert à convertir une chaîne en entier.

Code : C
1
long int strtol(const char *nptr, char **endptr, int base);

Le premier argument est la chaîne que l'on veut convertir, le deuxième n'est pas important, et le troisième est la base (généralement on utilise la base 10).

Si vous avez bien suivi, vous devriez être capable de convertir une chaîne avec cela.

strtod

Conversion en réel cette fois-ci.

Code : C
1
double strtod(const char *nptr, char **endptr);


Le deuxième argument n'est toujours pas important et le premier est la chaîne.
Maintenant, vous connaissez le nécessaire pour faire une saisie de données contrôlée.

Bonne programmation !

(Ce document est sous licence CC - Commons Deed)

Partager

34 commentaires pour "Réaliser des saisies sécurisées grâce à fgets"
Note moyenne : 3.67 / 4 (12 votes)
Pseudo Commentaire
Hors ligne luliloue # Posté le 19/08/2009 à 12:45:16

Bonjour,
Quand je saisie les instructions indiquées dans le tuto avec fgets dans visual c++ express (voir ci-dessous)
#include <stdio.h>

int main (void)
{
char chaine[20];

printf("Tapez une phrase : \n");
fgets(chaine, sizeof chaine, stdin);
printf("Vous avez tape : '%s'", chaine);

return 0;
}

ca ne me laisse pas faire une saisie clavier ça écrit direct:
Tapez une phrase:
Vous avez tape:

Es-ce à cause de l'IDE? ça semble marcher pour les autres utilisateurs donc je ne comprends pas
Quelqu'un peut m'aider??? :euh:
Merci d'avance :)
Hors ligne Napsters # Posté le 19/08/2009 à 17:05:47
Pouette pouette
Avatar

Heuu, dans apprendre à programmer en C (tuto officiel ;) )
Tout est expliquer, sa ne sert à rien là ;)


MumbleServ Location de mumble Payant et Gratuit
Speed-Pictures Hébergeur d'images

 
Hors ligne Rolandculer # Posté le 11/11/2009 à 16:10:44
Avatar

Études : UTBM

Bonjour, je suis débutant en c; et en effet j'ai des problèmes avec le scanf.
après avoir plusieur recherche non fructueuse je me décide a exposer mon problème.

Je souhaite saisir un entier compris entre 1 et 15, cependant si on saisie une lettre à la place d'un nombre, il n'y pas contre-indication.j'ai donc trouver une solution mais qui résoud en partie mon problème. j'utilise le fflush(stdin).

J'aimerais savoir si il y'a une méthode pas trop lourde pour sécuriser une saisie d'un entier.

merci d'avance!
Hors ligne ZeroSAMY # Posté le 11/04/2010 à 22:42:20
Avatar
Groupe : Bannis

comme en sais tous que fget et utiliser pour lire dans un ficher alors qui ce qui arriveras si je met sa au début FILE* fichier = NULL; sa va annuler son fonctionnement autan que scanf qui récupérer une chaîne de caractère avec des espace o_O ?
Hors ligne wawawou # Posté le 25/04/2011 à 18:12:45

Bonjours a tous.
Je suis aussi débutant en C et en galère pour mes cours donc je viens demandé conseil au connaisseur^^
Voila en gros voila mon énnoncé :D :

Le programme charge d'abord en MC, dans un tableau dynamique, tous les noms de fichier mp3 (il est plus simple de prévoir une chaîne de caractères dynamique par fichier et un autre tableau dynamique qui pointe vers ces chaînes). Ce chargement dans se faire dans une fonction avec passage de paramètre.
Ensuite, dans un menu, le programme propose à l'utilisateur de voir tous les fichiers mp3, de voir tous les fichiers mp3 d'un genre donné, de voir le genre d'un fichier donné.


je voudrais donc faire un tableau avec dans case un pointeur vers des chaine de caractère dynamique contenant le titre des mp3.
Pour ça je voudrais utilisé une fonction malloc, j'ai ceci comme code:

void chargeTab(FILE*f,T_Mp3){
char* temp;
char *posEnt = NULL;

int i;
T_Mp3*tableau;
tableau=(T_Mp3*)malloc((nb_lignes(f))*sizeof(T_Mp3));
if (tableau==NULL) {
printf("Erreur d'allocation memoire !!!");
exit(1);
}

if((f=fopen(PATH,"r"))==NULL)
printf("KO avec %s \n\n",PATH);
printf("\n\n Il y a %d fichier(s) \n\n",nb_lignes(F));

for (i=0;i<(nb_lignes(f));i++){
fseek(f, 0, SEEK_SET);
if(fgets(temp,31,f)!= NULL){
posEnt = strchr(temp, '\n');
if (posEnt != NULL) {
*posEnt = '\0';
}
}
tableau[i].titre=(char*)malloc(strlen(temp)*sizeof(char));
strcpy (tableau[i].titre,temp);
}

}


mais il me retourne a chaque fois comme erreur

error: incompatible types in assignnment of 'char*' to 'char[31]'

et j'ai beau cherché partotu je ne trouve pas mon erreur, quelqu'un pourrais m'aider?

Voir tous les commentaires