Version en ligne

Tutoriel : Réaliser des saisies sécurisées grâce à fgets

Table des matières

Réaliser des saisies sécurisées grâce à fgets
Étude de la fonction scanf()
Captures sécurisées grâce à fgets() et une fonction de conversion !

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

Étude de la fonction scanf()

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

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

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 :

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() !

#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 :

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 :

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.

#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 :

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 :

static void purger(void)
{
    int c;

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

On va tester ça avec un petit programme :

#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 :

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 !

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

Étude de la fonction scanf()

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 :

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 ' ' (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.

Problèmes

Voilà, vous avez les connaissances pour utiliser fgets() correctement. On va mettre cela en pratique :

#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 :

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 :

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 et on le supprime (grâce au pointeur, on le déférence).

Un petit test :

#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;
}
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 :

static void purger(void)
{
    int c;

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

Un petit test :

#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;
}
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

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 :

#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.

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.

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)


Étude de la fonction scanf()