Saisie « sécurisée » dans un terminal

a marqué ce sujet comme résolu.

Bonjour,

J’ai travaillé à développer des petites fonctions aidant à réaliser une saisie par l’utilisateur d’une ligne de caractères, d’un entier ou d’un nombre à virgule, en évitant les problèmes de débordement mémoire.

Je me suis basé sur cet article (que j’ai lu il y a quelques années) et je me suis aidé de l’excellent cours sur le langage C de Zeste de Savoir.

Je souhaiterais avoir vos retours sur d’éventuelles améliorations à leur apporter, ou des discussions sur les choix que j’ai pu faire.

Une autre question que je me posais : dans le cas où j’utiliserais ces fonctions dans un projet personnel public, serait-il approprié que j’indique en être l’auteur et par conséquent y apposer une licence (la CeCILL-B pour les curieux) ?

Enfin, s’il existe une bibliothèque légère qui permet de remplir les besoins visés par ces fonctions de manière plus efficace, je serais heureux de la connaître.

+0 -0

Salut,

Je souhaiterais avoir vos retours sur d’éventuelles améliorations à leur apporter, ou des discussions sur les choix que j’ai pu faire.

Ifrit

Voici les remarques que je peux faire :

Fonction lireLigne

Tu emploies une variable lu qui n’est pas définie.

Fonction lireEntier
  1. Je te suggère d’ajouter une assertion vérifiant que longueur a une valeur supérieure à zéro ;
  2. A priori, l’allocation dynamique n’est pas nécessaire ici vu la petite taille (qui peut être finalement fixe) nécessaire, un tableau de classe de stockage statique (ou non) est suffisant ;
  3. La condition if (nombreDansChaine == finDeChaine) n’est pas correcte, cela devrait plutôt être if (*finDeChaine != '\0') afin de garantir que strtol() a parcouru toute la chaîne ;
  4. Pourquoi ne pas utiliser scanf() ?
Fonction lireFlottant

Mêmes remarques que pour lireEntier().

Une autre question que je me posais : dans le cas où j’utiliserais ces fonctions dans un projet personnel public, serait-il approprié que j’indique en être l’auteur et par conséquent y apposer une licence (la CeCILL-B pour les curieux) ?

Ifrit

Il est effectivement important que tu indiques si tu octroies ou non une licence aux potentiels utilisateurs de ton code sans quoi cela signifie que le droit d’auteur s’applique à pur et à plein.

Enfin, s’il existe une bibliothèque légère qui permet de remplir les besoins visés par ces fonctions de manière plus efficace, je serais heureux de la connaître.

Ifrit

Á ma connaisance, non. Étant donné que la vidange du tampon n’est pas toujours souhaitable et que les besoins varient, cela est laissé aux bons soins du programmeurs.

+0 -0

lireLigne()

Il n’y pas de variable lu, c’est le chiffre 1 suivi de la lettre u.

lireEntier()

  1. J’ai supposé que l’utilisateur de ces fonctions était en charge de la valeur correcte des arguments, mais peut-être que dans un objectif de débogage c’est effectivement utile. Dans ce cas, il faudrait aussi ajouter cette assertion dans la fonction lireLigne(), non ?
  2. Le souci c’est que la fonction lireEntier() appelle la fonction lireLigne() qui elle-même appelle la fonction fgets() qui demande un paramètre pour la longueur de la chaîne. C’est vrai que pour la saisie d’un seul entier le tableau ne sera pas très grand, mais fixer une taille à l’avance me semble moins adapté. En fait, je ne vois le désavantage à faire de l’allocation dynamique ici.
  3. J’ai rajouté cette condition pour gérer le cas où l’utilisateur saisi 0, et lors de mes recherches c’est bien cette condition qui est indiquée pour gérer ce cas. J’ai testé ce cas et cela fonctionne bien. Voir ici et ici.
  4. Mmh… C’est vrai que pour saisir un seul entier ou un seul flottant, scanf() (voire scanf_s()) paraît plus simple. Dans le cas où l’utilisateur saisi n’importe quoi, il me suffit de vider la mémoire tampon et de vérifier la valeur renvoyée par scanf(), c’est bien ça ?

lireFlottant

Mêmes réponses que pour lireEntier(). :D

Pour la licence, je suis bien conscient de l’importance de l’indiquer, je me demandais en fait si je pouvais le faire dans ce cas puisque je me suis largement basé sur l’article de feu Site du Zéro pour écrire ces fonctions. Ou bien sont-elles trop triviales pour qu’une quelconque mention de licence ait réellement un effet et donc autant la mettre ?

+0 -0

Il n’y pas de variable lu, c’est le chiffre 1 suivi de la lettre u.

Ifrit

Arf ! Désolé, la police d’écriture m’a induit en erreur. >_<

  1. J’ai supposé que l’utilisateur de ces fonctions était en charge de la valeur correcte des arguments, mais peut-être qu dans un objectif de débogage c’est effectivement utile. Dans ce cas, il faudrait aussi ajouter cette assertion dans la fonction lireLigne(), non ?
Ifrit

Justement : si c’est à l’utilisateur qu’il incombe d’envoyer une valeur valide (et donc de vérifier cela avant l’appel), alors une assertion est adaptée afin de détecter les cas où cette vérifications n’a pas eu lieu. ;)
Sinon, oui, la même assertion peut être employée pour la fonction lireLigne().

  1. Le souci c’est que la fonction lireEntier() appelle la fonction lireLigne() qui elle-même appelle la fonction fgets() qui demande un paramètre pour la longueur de la chaîne. C’est vrai que pour la saisie d’un seul entier le tableau ne sera pas très grand, mais fixer une taille à l’avance me semble moins adapté. En fait, je ne vois le désavantage à faire de l’allocation dynamique ici.
Ifrit

Ben, disons que recourir à l’allocation dynamique est d’une part plus coûteux (en terme de ressource) que d’employer un tableau alloué automatiquement ou statiquement et, d’autre part, complexifie ton code. Maintenant, libre à toi de l’employer tout de même.

  1. J’ai rajouté cette condition pour gérer le cas où l’utilisateur saisi 0, et lors de mes recherches c’est bien cette condition qui est indiquée pour gérer ce cas. J’ai testé ce cas et cela fonctionne bien. Voir ici et ici.
Ifrit

Je ne comprends pas : 0 est une valeur valide, donc pourquoi retournes-tu un code d’erreur dans ce cas ? Sinon, la condition que je te suggères te permets de gérer les entrées du type 65hello alors que la base est par exemple 10.

  1. Mmh… C’est vrai que pour saisir un seul entier ou un seul flottant, scanf() (voire scanf_s()) paraît plus simple. Dans le cas où l’utilisateur saisi n’importe quoi, il me suffit de vider la mémoire tampon et de vérifier la valeur renvoyée par scanf(), c’est bien ça ?
Ifrit

Yep.

Pour la licence, je suis bien conscient de l’importance de l’indiquer, je me demandais en fait si je pouvais le faire dans ce cas puisque je me suis largement basé sur l’article de feu Site du Zéro pour écrire ces fonctions. Ou bien sont-elles trop triviales pour qu’une quelconque mention de licence ait réellement un effet et donc autant la mettre ?

Ifrit

Il y a effectivement peu de chances qu’un code de ce type soit reconnu comme « originale » (au sens du droit d’auteur), mais bon, ne rien mettre signifie bien que le droit d’auteur s’applique normalement.

+0 -0

Oh mais j’y pense, en C11 on peut déclarer un tableau de classe automatique en utilisant une variable (ici longueur) pour sa taille ? En tout cas je viens de tester et il n’y a pas d’erreur à la compilation. Si c’est le cas, faire une allocation dynamique est effectivement inutile.

La condition (nombreDansChaine == finDeChaine) servait effectivement à distinguer le cas où l’utilisateur saisi la valeur 0 qui est bien valide, et le cas où strtol() renvoie la valeur 0 car aucune conversion n’a eu lieu. Mais effectivement, ta condition est meilleure car elle prend en compte les saisies du type 65hello, tout en permettant toujours de distinguer un 0 saisi par l’utilisateur d’un 0 renvoyé par la fonction.

D’ailleurs, scanf() ne permet pas de gérer les saisies du type 65hello (en tout cas pas simplement apparemment).

+0 -0

Oh mais j’y pense, en C11 on peut déclarer un tableau de classe automatique en utilisant une variable (ici longueur) pour sa taille ? En tout cas je viens de tester et il n’y a pas d’erreur à la compilation. Si c’est le cas, faire une allocation dynamique est effectivement inutile.

Ifrit

C’est autorisé depuis la norme C99, mais je te le déconseille parce qu’il est impossible de savoir si l’allocation a réussi ou non (celle-ci étant le plus souvent réalisée sur la pile).

D’ailleurs, scanf() ne permet pas de gérer les saisies du type 65hello (en tout cas pas simplement apparemment).

Ifrit

Il est possible de gérer cela avec scanf() en vérifiant si la fin de ligne a été atteinte.

 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
#include <stdio.h>
#include <stdlib.h>


int
main(void)
{
    int n;
    int c;

    if (scanf("%d", &n) < 0) {
        perror("scanf");
        return EXIT_FAILURE;
    }

    c = getchar();

    if (ferror(stdin)) {
        perror("getchar");
        return EXIT_FAILURE;
    } else if (c != '\n' && c != EOF) {
        fprintf(stderr, "Mauvaise saisie.\n");
        return EXIT_FAILURE;
    }

    printf("%d.\n", n);
    return 0;
}

Note : je ne vidange pas le tampon ici puisque le programme se termine après la saisie, mais il est bien entendu nécessaire de le faire si tu as d’autres saisies à réaliser par après.

+0 -0

Je pensais répondre quelques jours après ton dernier message, mais il s’est déjà écoulé presque deux mois !

Alors effectivement les tableaux de taille variable posent problème pour plusieurs raisons, et la plus importante pour moi est que leur prise en charge est optionnelle en C11 (voir ici pour plus de détails).

Cela dit, fixer une taille à l’avance m’ennuie. En conséquence, je vais rester avec l’allocation dynamique.

Quant à scanf(), le problème est que lorsque la conversion n’a pas eu lieu, le message renvoyé par perror est « scanf(): Success », ce qui est pour le moins douteux… Je vais donc rester avec ces petites fonctions qui me semblent plus explicites quand à leurs messages d’erreur.

Merci beaucoup pour l’aide et les remarques, si tu en as d’autres n’hésite pas. Voici la dernière version de mes fonctions.

+0 -0

Quant à scanf(), le problème est que lorsque la conversion n’a pas eu lieu, le message renvoyé par perror est « scanf(): Success », ce qui est pour le moins douteux…

Ifrit

Mmm… Peux-tu me montrer un exemple qui te donne ce comportement ?

+0 -0

Si perror affiche "success", ça veut dire que tu as détecté une erreur qui n’en est pas une pour les fonctions standards.

Dans tes conditions qui détectent une erreur, perror est à utiliser quand c’est une fonction standard (ou une fonction système, pour la programmation système) qui provoque l’erreur. Si tu détectes une erreur qui n’a pas été provoquée par une fonction standard mais parce que, par exemple, tu détectes que l’utilisateur entre n’importe quoi, utilise plutôt fprintf(stderr, ton_message_d_erreur);.

Je te conseille d’aller voir le manuel de perror (tape "man perror" dans un moteur de recherche) pour bien comprendre ce qu’elle fait. Pour résumer, elle affiche l’erreur correspondant à la valeur contenu par la variable globale errno (du fichier errno.h), à la suite de la chaine que tu lui passes en paramètre.

+0 -0

Voici le morceau de code que j’ai utilisé pour tester scanf() :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
printf("Saisir entier : ");

if (scanf("%ld", &entierSaisi) != 1)
{
    perror("scanf()");
    return EXIT_FAILURE;
}

viderTampon();
printf("Vous avez saisi %ld.\n", entierSaisi);

printf("Saisir flottant : ");

if (scanf("%lg", &flottantSaisi) != 1)
{
    perror("scanf()");
    return EXIT_FAILURE;
}

viderTampon();
printf("Vous avez saisi %lg.\n", flottantSaisi);

Si je saisi he65llo, j’obtiens le message scanf(): Success et le programme se termine immédiatement.

De ce que j’ai compris, scanf() renvoie le nombre de conversion réussie. Si elle ne n’affecte pas un code d’erreur à la variable errno en même qu’elle renvoie un nombre de conversion réussie qui n’est pas égal au nombre de variables passées en paramètre, elle est bien peu pratique à utiliser à mon avis.

+0 -0

En fait, c’est en un sens assez logique. En effet, il n’y a pas à proprement parler d’erreur dans ce cas ci, dans le sens où il s’agit d’un comportement « normal » de la fonction scanf() : aucune conversion n’a pu être réalisée au vu de l’entrée fournie (ce qui n’est pas « grave », en soit). Ceci est à distinguer d’un cas d’erreur où, par exemple, aucune donnée n’a pu être récupérée depuis le terminal à cause d’une erreur matérielle.

Cela étant, si tu souhaites éviter ce type de message, il va te falloir distinguer les deux cas à l’aide de la fonction ferror().

1
2
3
4
5
6
7
8
9
if (scanf("%ld", &entierSaisi) != 1)
{
    if (ferror(stdin))
        perror("scanf()");
    else
        fprintf(stderr, "Mauvaise saisie.\n");

    return EXIT_FAILURE;
}
+1 -0

En effet, une erreur indiquée par errno est provoquée par des erreurs du style : mauvaise adresse de pointeur, échec d’allocation, mauvais descripteur de fichier, échec d’ouverture d’une ressource (fichier, socket, base de données)…
Bref, des erreurs difficilement récupérable qui provoque en général une annulation de la tâche en cours (s’il y a une gestion d’erreurs), voir un crash de l’application.

J’espère ne pas dire de bêtises

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte